This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new ad7952fdc feat: add override_content_type (#2734)
ad7952fdc is described below

commit ad7952fdcfe5ab9ca4b4b921df2df6768a035a80
Author: G-XD <[email protected]>
AuthorDate: Mon Jul 31 12:30:40 2023 +0800

    feat: add override_content_type (#2734)
    
    * feat: add override_content_type
    
    * refactor: replace s3_get_object/s3_get_object_request params with OpRead
    
    ---------
    
    Co-authored-by: GXD <[email protected]>
---
 core/src/raw/ops.rs                         | 12 +++++++
 core/src/services/s3/backend.rs             | 21 ++-----------
 core/src/services/s3/core.rs                | 41 ++++++++++--------------
 core/src/types/capability.rs                |  2 ++
 core/src/types/operator/operator_futures.rs | 24 ++++++++++++++
 core/tests/behavior/write.rs                | 49 +++++++++++++++++++++++++++++
 6 files changed, 106 insertions(+), 43 deletions(-)

diff --git a/core/src/raw/ops.rs b/core/src/raw/ops.rs
index 16194b0d7..308930b5c 100644
--- a/core/src/raw/ops.rs
+++ b/core/src/raw/ops.rs
@@ -242,6 +242,7 @@ pub struct OpRead {
     br: BytesRange,
     if_match: Option<String>,
     if_none_match: Option<String>,
+    override_content_type: Option<String>,
     override_cache_control: Option<String>,
     override_content_disposition: Option<String>,
     version: Option<String>,
@@ -287,6 +288,17 @@ impl OpRead {
         self.override_cache_control.as_deref()
     }
 
+    /// Sets the content-type header that should be send back by the remote 
read operation.
+    pub fn with_override_content_type(mut self, content_type: &str) -> Self {
+        self.override_content_type = Some(content_type.into());
+        self
+    }
+
+    /// Returns the content-type header that should be send back by the remote 
read operation.
+    pub fn override_content_type(&self) -> Option<&str> {
+        self.override_content_type.as_deref()
+    }
+
     /// Set the If-Match of the option
     pub fn with_if_match(mut self, if_match: &str) -> Self {
         self.if_match = Some(if_match.to_string());
diff --git a/core/src/services/s3/backend.rs b/core/src/services/s3/backend.rs
index fdf4e4f84..420f9b081 100644
--- a/core/src/services/s3/backend.rs
+++ b/core/src/services/s3/backend.rs
@@ -921,6 +921,7 @@ impl Accessor for S3Backend {
                 read_with_if_none_match: true,
                 read_with_override_cache_control: true,
                 read_with_override_content_disposition: true,
+                read_with_override_content_type: true,
 
                 write: true,
                 write_can_sink: true,
@@ -972,16 +973,7 @@ impl Accessor for S3Backend {
     }
 
     async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, 
Self::Reader)> {
-        let resp = self
-            .core
-            .s3_get_object(
-                path,
-                args.range(),
-                args.if_none_match(),
-                args.if_match(),
-                args.override_content_disposition(),
-            )
-            .await?;
+        let resp = self.core.s3_get_object(path, args).await?;
 
         let status = resp.status();
 
@@ -1071,14 +1063,7 @@ impl Accessor for S3Backend {
                 self.core
                     .s3_head_object_request(path, v.if_none_match(), 
v.if_match())?
             }
-            PresignOperation::Read(v) => self.core.s3_get_object_request(
-                path,
-                v.range(),
-                v.override_content_disposition(),
-                v.override_cache_control(),
-                v.if_none_match(),
-                v.if_match(),
-            )?,
+            PresignOperation::Read(v) => self.core.s3_get_object_request(path, 
v.clone())?,
             PresignOperation::Write(_) => {
                 self.core
                     .s3_put_object_request(path, None, None, None, None, 
AsyncBody::Empty)?
diff --git a/core/src/services/s3/core.rs b/core/src/services/s3/core.rs
index e06c1e6c6..36321280f 100644
--- a/core/src/services/s3/core.rs
+++ b/core/src/services/s3/core.rs
@@ -63,6 +63,7 @@ mod constants {
         "x-amz-copy-source-server-side-encryption-customer-key-md5";
 
     pub const RESPONSE_CONTENT_DISPOSITION: &str = 
"response-content-disposition";
+    pub const RESPONSE_CONTENT_TYPE: &str = "response-content-type";
     pub const RESPONSE_CACHE_CONTROL: &str = "response-cache-control";
 }
 
@@ -238,15 +239,7 @@ impl S3Core {
         Ok(req)
     }
 
-    pub fn s3_get_object_request(
-        &self,
-        path: &str,
-        range: BytesRange,
-        override_content_disposition: Option<&str>,
-        override_cache_control: Option<&str>,
-        if_none_match: Option<&str>,
-        if_match: Option<&str>,
-    ) -> Result<Request<AsyncBody>> {
+    pub fn s3_get_object_request(&self, path: &str, args: OpRead) -> 
Result<Request<AsyncBody>> {
         let p = build_abs_path(&self.root, path);
 
         // Construct headers to add to the request
@@ -254,14 +247,21 @@ impl S3Core {
 
         // Add query arguments to the URL based on response overrides
         let mut query_args = Vec::new();
-        if let Some(override_content_disposition) = 
override_content_disposition {
+        if let Some(override_content_disposition) = 
args.override_content_disposition() {
             query_args.push(format!(
                 "{}={}",
                 constants::RESPONSE_CONTENT_DISPOSITION,
                 percent_encode_path(override_content_disposition)
             ))
         }
-        if let Some(override_cache_control) = override_cache_control {
+        if let Some(override_content_type) = args.override_content_type() {
+            query_args.push(format!(
+                "{}={}",
+                constants::RESPONSE_CONTENT_TYPE,
+                percent_encode_path(override_content_type)
+            ))
+        }
+        if let Some(override_cache_control) = args.override_cache_control() {
             query_args.push(format!(
                 "{}={}",
                 constants::RESPONSE_CACHE_CONTROL,
@@ -274,15 +274,16 @@ impl S3Core {
 
         let mut req = Request::get(&url);
 
+        let range = args.range();
         if !range.is_full() {
             req = req.header(http::header::RANGE, range.to_header());
         }
 
-        if let Some(if_none_match) = if_none_match {
+        if let Some(if_none_match) = args.if_none_match() {
             req = req.header(IF_NONE_MATCH, if_none_match);
         }
 
-        if let Some(if_match) = if_match {
+        if let Some(if_match) = args.if_match() {
             req = req.header(IF_MATCH, if_match);
         }
         // Set SSE headers.
@@ -299,19 +300,9 @@ impl S3Core {
     pub async fn s3_get_object(
         &self,
         path: &str,
-        range: BytesRange,
-        if_none_match: Option<&str>,
-        if_match: Option<&str>,
-        override_content_disposition: Option<&str>,
+        args: OpRead,
     ) -> Result<Response<IncomingAsyncBody>> {
-        let mut req = self.s3_get_object_request(
-            path,
-            range,
-            override_content_disposition,
-            None,
-            if_none_match,
-            if_match,
-        )?;
+        let mut req = self.s3_get_object_request(path, args)?;
 
         self.sign(&mut req).await?;
 
diff --git a/core/src/types/capability.rs b/core/src/types/capability.rs
index ba3ed57c5..de91bc076 100644
--- a/core/src/types/capability.rs
+++ b/core/src/types/capability.rs
@@ -71,6 +71,8 @@ pub struct Capability {
     pub read_with_override_cache_control: bool,
     /// if operator supports read with override content disposition natively, 
it will be true.
     pub read_with_override_content_disposition: bool,
+    /// if operator supports read with override content type natively, it will 
be true.
+    pub read_with_override_content_type: bool,
 
     /// If operator supports write natively, it will be true.
     pub write: bool,
diff --git a/core/src/types/operator/operator_futures.rs 
b/core/src/types/operator/operator_futures.rs
index 6ab43d17a..8fd52a31e 100644
--- a/core/src/types/operator/operator_futures.rs
+++ b/core/src/types/operator/operator_futures.rs
@@ -251,6 +251,14 @@ impl FuturePresignRead {
         self
     }
 
+    /// Sets the content-type header that should be send back by the remote 
read operation.
+    pub fn override_content_type(mut self, v: &str) -> Self {
+        self.0 = self
+            .0
+            .map_args(|(args, dur)| (args.with_override_content_type(v), dur));
+        self
+    }
+
     /// Set the If-Match of the option
     pub fn if_match(mut self, v: &str) -> Self {
         self.0 = self.0.map_args(|(args, dur)| (args.with_if_match(v), dur));
@@ -352,6 +360,14 @@ impl FutureRead {
         self
     }
 
+    /// Sets the content-type header that should be send back by the remote 
read operation.
+    pub fn override_content_type(mut self, content_type: &str) -> Self {
+        self.0 = self
+            .0
+            .map_args(|args| args.with_override_content_type(content_type));
+        self
+    }
+
     /// Set the If-Match for this operation.
     pub fn if_match(mut self, v: &str) -> Self {
         self.0 = self.0.map_args(|args| args.with_if_match(v));
@@ -407,6 +423,14 @@ impl FutureReader {
         self
     }
 
+    /// Sets the content-type header that should be send back by the remote 
read operation.
+    pub fn override_content_type(mut self, content_type: &str) -> Self {
+        self.0 = self
+            .0
+            .map_args(|args| args.with_override_content_type(content_type));
+        self
+    }
+
     /// Set the If-Match for this operation.
     pub fn if_match(mut self, v: &str) -> Self {
         self.0 = self.0.map_args(|args| args.with_if_match(v));
diff --git a/core/tests/behavior/write.rs b/core/tests/behavior/write.rs
index 4ef2a6208..53ee84581 100644
--- a/core/tests/behavior/write.rs
+++ b/core/tests/behavior/write.rs
@@ -75,6 +75,7 @@ pub fn behavior_write_tests(op: &Operator) -> Vec<Trial> {
         test_read_with_special_chars,
         test_read_with_override_cache_control,
         test_read_with_override_content_disposition,
+        test_read_with_override_content_type,
         test_delete_file,
         test_delete_empty_dir,
         test_delete_with_special_chars,
@@ -927,6 +928,54 @@ pub async fn 
test_read_with_override_content_disposition(op: Operator) -> Result
     Ok(())
 }
 
+/// Read file with override_content_type should succeed.
+pub async fn test_read_with_override_content_type(op: Operator) -> Result<()> {
+    if !(op.info().capability().read_with_override_content_type && 
op.info().can_presign()) {
+        return Ok(());
+    }
+
+    let path = uuid::Uuid::new_v4().to_string();
+    let (content, _) = gen_bytes();
+
+    op.write(&path, content.clone())
+        .await
+        .expect("write must succeed");
+
+    let target_content_type = "application/opendal";
+
+    let signed_req = op
+        .presign_read_with(&path, Duration::from_secs(60))
+        .override_content_type(target_content_type)
+        .await
+        .expect("presign must succeed");
+
+    let client = reqwest::Client::new();
+    let mut req = client.request(
+        signed_req.method().clone(),
+        Url::from_str(&signed_req.uri().to_string()).expect("must be valid 
url"),
+    );
+    for (k, v) in signed_req.header() {
+        req = req.header(k, v);
+    }
+
+    let resp = req.send().await.expect("send must succeed");
+
+    assert_eq!(resp.status(), StatusCode::OK);
+    assert_eq!(
+        resp.headers()
+            .get(http::header::CONTENT_TYPE)
+            .expect("content-type header must exist")
+            .to_str()
+            .expect("content-type header must be string"),
+        target_content_type
+    );
+    assert_eq!(resp.bytes().await?, content);
+
+    op.delete(&path).await.expect("delete must succeed");
+
+    Ok(())
+}
+
 /// Delete existing file should succeed.
 pub async fn test_writer_abort(op: Operator) -> Result<()> {
     let path = uuid::Uuid::new_v4().to_string();

Reply via email to