This is an automated email from the ASF dual-hosted git repository.
psiace pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal-oli.git
The following commit(s) were added to refs/heads/main by this push:
new 8158247 feat: add cp content-type flag and enable mime-guess layer
(#7)
8158247 is described below
commit 8158247f33a58b104244f20dc30ab04c87a93655
Author: Chojan Shang <[email protected]>
AuthorDate: Thu Nov 20 11:37:58 2025 +0800
feat: add cp content-type flag and enable mime-guess layer (#7)
Signed-off-by: Chojan Shang <[email protected]>
---
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 -----