You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by ps...@apache.org on 2025/11/19 18:28:53 UTC
(opendal-oli) branch feat/mime-guess created (now 0d878d6)
This is an automated email from the ASF dual-hosted git repository.
psiace pushed a change to branch feat/mime-guess
in repository https://gitbox.apache.org/repos/asf/opendal-oli.git
at 0d878d6 feat: add cp content-type flag and enable mime-guess layer
This branch includes the following new commits:
new 0d878d6 feat: add cp content-type flag and enable mime-guess layer
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(opendal-oli) 01/01: feat: add cp content-type flag and enable mime-guess layer
Posted by ps...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
psiace pushed a commit to branch feat/mime-guess
in repository https://gitbox.apache.org/repos/asf/opendal-oli.git
commit 0d878d65d472422b7181b67d82c87c7c54efefd1
Author: Chojan Shang <ch...@vesoft.com>
AuthorDate: Thu Nov 20 02:28:30 2025 +0800
feat: add cp content-type flag and enable mime-guess layer
Signed-off-by: Chojan Shang <ch...@vesoft.com>
---
Cargo.lock | 23 ++++++++++++++++++++
Cargo.toml | 1 +
README.md | 3 +++
src/commands/cp.rs | 54 +++++++++++++++++++++++++++++++++++------------
src/config/mod.rs | 30 ++++++++++++++++++++++++--
tests/integration/stat.rs | 2 ++
6 files changed, 97 insertions(+), 16 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 1845488..ab7ce87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1251,6 +1251,22 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
[[package]]
name = "miniz_oxide"
version = "0.8.8"
@@ -1397,6 +1413,7 @@ dependencies = [
"http-body",
"log",
"md-5",
+ "mime_guess",
"percent-encoding",
"prost",
"quick-xml 0.38.0",
@@ -2583,6 +2600,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+[[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
[[package]]
name = "unicode-ident"
version = "1.0.18"
diff --git a/Cargo.toml b/Cargo.toml
index b87d95b..52305b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -54,6 +54,7 @@ opendal = { version = "0.54.0", features = [
"services-webhdfs",
"services-azfile",
"services-dropbox",
+ "layers-mime-guess",
] }
parse-size = { version = "1.1" }
pollster = { version = "0.4" }
diff --git a/README.md b/README.md
index fd067eb..a0140bd 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,9 @@ oli cp ./update-ecs-loadbalancer.json s3:/update-ecs-loadbalancer.json
oli ls s3:/
# fleet.png
# update-ecs-loadbalancer.json
+
+# Override the inferred MIME type when needed:
+oli cp --content-type application/json ./update-ecs-loadbalancer.json s3:/update-ecs-loadbalancer.json
```
### Example: use `oli` copy file from S3 to R2
diff --git a/src/commands/cp.rs b/src/commands/cp.rs
index 52d876d..a0bb2cf 100644
--- a/src/commands/cp.rs
+++ b/src/commands/cp.rs
@@ -62,6 +62,9 @@ pub struct CopyCmd {
/// Copy objects recursively.
#[arg(short = 'r', long)]
pub recursive: bool,
+ /// Explicit content type for destination objects.
+ #[arg(long = "content-type")]
+ pub content_type: Option<String>,
}
impl CopyCmd {
@@ -70,14 +73,22 @@ impl CopyCmd {
}
async fn do_run(self) -> Result<()> {
- let cfg = Config::load(&self.config_params.config)?;
+ let CopyCmd {
+ config_params,
+ source,
+ destination,
+ recursive,
+ content_type,
+ } = self;
- let (src_op, src_path) = cfg.parse_location(&self.source)?;
- let (dst_op, dst_path) = cfg.parse_location(&self.destination)?;
+ let cfg = Config::load(&config_params.config)?;
+
+ let (src_op, src_path) = cfg.parse_location(&source)?;
+ let (dst_op, dst_path) = cfg.parse_location(&destination)?;
let final_dst_path = match dst_op.stat(&dst_path).await {
Ok(dst_meta) if dst_meta.mode().is_dir() => {
- if self.recursive {
+ if recursive {
dst_path.clone()
} else if let Some(filename) = Path::new(&src_path).file_name() {
Path::new(&dst_path)
@@ -95,7 +106,7 @@ impl CopyCmd {
Ok(_) => {
// Destination exists but is a file. Overwrite it (non-recursive)
// or error (recursive, handled below).
- if self.recursive {
+ if recursive {
bail!(
"Recursive copy destination '{}' exists but is not a directory.",
dst_path
@@ -109,12 +120,13 @@ impl CopyCmd {
}
};
- if !self.recursive {
+ if !recursive {
// Non-recursive copy: Use the final_dst_path directly.
- let mut dst_w = dst_op
- .writer(&final_dst_path)
- .await?
- .into_futures_async_write();
+ let mut dst_builder = dst_op.writer_with(&final_dst_path);
+ if let Some(ref ct) = content_type {
+ dst_builder = dst_builder.content_type(ct);
+ }
+ let mut dst_w = dst_builder.await?.into_futures_async_write();
let src_meta = src_op.stat(&src_path).await?;
let reader = src_op.reader_with(&src_path).chunk(8 * 1024 * 1024).await?;
let buf_reader = reader
@@ -204,10 +216,11 @@ impl CopyCmd {
.await?;
let copy_progress = CopyProgress::new(&fresh_meta, depath.to_string());
- let mut writer = dst_op
- .writer(¤t_dst_path)
- .await?
- .into_futures_async_write();
+ let mut writer_builder = dst_op.writer_with(¤t_dst_path);
+ if let Some(ref ct) = content_type {
+ writer_builder = writer_builder.content_type(ct);
+ }
+ let mut writer = writer_builder.await?.into_futures_async_write();
copy_progress.copy(buf_reader, &mut writer).await?;
writer.close().await?;
@@ -297,6 +310,7 @@ fn relative_path_from_entry(
#[cfg(test)]
mod tests {
use super::*;
+ use clap::Parser;
#[test]
fn normalize_path_strips_root_and_current_components() {
@@ -323,4 +337,16 @@ mod tests {
.expect_err("expected prefix mismatch");
assert!(format!("{err}").contains("does not start with source path"));
}
+
+ #[test]
+ fn parses_content_type_flag() {
+ let cmd = CopyCmd::parse_from([
+ "cp",
+ "--content-type",
+ "text/plain",
+ "src_profile:/foo",
+ "dst_profile:/bar",
+ ]);
+ assert_eq!(cmd.content_type.as_deref(), Some("text/plain"));
+ }
}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 97ed9fa..d6e467d 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -28,6 +28,7 @@ use anyhow::Result;
use anyhow::anyhow;
use opendal::Operator;
use opendal::Scheme;
+use opendal::layers::MimeGuessLayer;
use opendal::services;
use serde::Deserialize;
use url::Url;
@@ -142,7 +143,12 @@ impl Config {
}
};
- return Ok((Operator::new(fs_builder)?.finish(), filename.into()));
+ return Ok((
+ Operator::new(fs_builder)?
+ .layer(MimeGuessLayer::default())
+ .finish(),
+ filename.into(),
+ ));
}
let location = Url::parse(s)?;
@@ -166,12 +172,15 @@ impl Config {
.get("type")
.ok_or_else(|| anyhow!("missing 'type' in profile"))?;
let scheme = Scheme::from_str(svc)?;
- Ok(Operator::via_iter(scheme, profile.clone())?)
+ let op = Operator::via_iter(scheme, profile.clone())?.layer(MimeGuessLayer::default());
+ Ok(op)
}
}
#[cfg(test)]
mod tests {
+ use std::collections::HashMap;
+
use opendal::Scheme;
use super::*;
@@ -207,6 +216,23 @@ mod tests {
}
}
+ #[tokio::test]
+ async fn operator_uses_mime_guess_layer() -> Result<()> {
+ let mut memory_profile = HashMap::new();
+ memory_profile.insert("type".to_string(), "memory".to_string());
+
+ let mut profiles = HashMap::new();
+ profiles.insert("mem".to_string(), memory_profile);
+
+ let cfg = Config { profiles };
+ let op = cfg.operator("mem")?;
+ op.write("demo.json", "{}").await?;
+
+ let meta = op.stat("demo.json").await?;
+ assert_eq!(meta.content_type(), Some("application/json"));
+ Ok(())
+ }
+
#[test]
fn test_load_from_toml() -> Result<()> {
let dir = tempfile::tempdir()?;
diff --git a/tests/integration/stat.rs b/tests/integration/stat.rs
index 3cea0ac..0f6ae68 100644
--- a/tests/integration/stat.rs
+++ b/tests/integration/stat.rs
@@ -34,6 +34,7 @@ async fn test_basic_stat() -> Result<()> {
path: [TEMP_DIR]/dst.txt
size: 5
type: file
+ content-type: text/plain
last-modified: [TIMESTAMP]
----- stderr -----
@@ -56,6 +57,7 @@ async fn test_stat_for_path_in_current_dir() -> Result<()> {
path: dst.txt
size: 5
type: file
+ content-type: text/plain
last-modified: [TIMESTAMP]
----- stderr -----