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/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new a71ce00c1 feat(services/b2): Add user defined metadata support (#6844)
a71ce00c1 is described below

commit a71ce00c1441c14577c2eb1fbc79314d546a040d
Author: zhan7236 <[email protected]>
AuthorDate: Wed Dec 17 20:52:36 2025 +0800

    feat(services/b2): Add user defined metadata support (#6844)
    
    - Add X_BZ_INFO_PREFIX constant for user metadata HTTP header prefix
    - Add user metadata headers support in upload_file function
    - Add file_info field and user metadata support in start_large_file function
    - Update StartLargeFileRequest struct with optional file_info field
    - Update File struct with file_info field for parsing user metadata
    - Update parse_file_info to decode and return user metadata
    - Enable write_with_user_metadata capability
    
    Part of #4842.
---
 core/services/b2/src/backend.rs |  1 +
 core/services/b2/src/core.rs    | 48 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 49 insertions(+)

diff --git a/core/services/b2/src/backend.rs b/core/services/b2/src/backend.rs
index c1f0936f2..5f1d0fea8 100644
--- a/core/services/b2/src/backend.rs
+++ b/core/services/b2/src/backend.rs
@@ -173,6 +173,7 @@ impl Builder for B2Builder {
                             write_can_empty: true,
                             write_can_multi: true,
                             write_with_content_type: true,
+                            write_with_user_metadata: true,
                             // The min multipart size of b2 is 5 MiB.
                             //
                             // ref: 
<https://www.backblaze.com/docs/cloud-storage-large-files>
diff --git a/core/services/b2/src/core.rs b/core/services/b2/src/core.rs
index 4a6aac756..1e6b311d5 100644
--- a/core/services/b2/src/core.rs
+++ b/core/services/b2/src/core.rs
@@ -15,6 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use std::collections::HashMap;
 use std::fmt::Debug;
 use std::sync::Arc;
 
@@ -38,6 +39,7 @@ pub(super) mod constants {
     pub const X_BZ_FILE_NAME: &str = "X-Bz-File-Name";
     pub const X_BZ_CONTENT_SHA1: &str = "X-Bz-Content-Sha1";
     pub const X_BZ_PART_NUMBER: &str = "X-Bz-Part-Number";
+    pub const X_BZ_INFO_PREFIX: &str = "X-Bz-Info-";
 }
 
 /// Core of [b2](https://www.backblaze.com/cloud-storage) services support.
@@ -263,6 +265,17 @@ impl B2Core {
             req = req.header(header::CONTENT_DISPOSITION, pos)
         }
 
+        // Set user metadata headers.
+        // B2 uses X-Bz-Info-* prefix for custom file info.
+        if let Some(user_metadata) = args.user_metadata() {
+            for (key, value) in user_metadata {
+                req = req.header(
+                    format!("{}{}", constants::X_BZ_INFO_PREFIX, key),
+                    percent_encode_path(value),
+                );
+            }
+        }
+
         let req = req.extension(Operation::Write);
 
         // Set body
@@ -286,12 +299,23 @@ impl B2Core {
             bucket_id: self.bucket_id.clone(),
             file_name: percent_encode_path(&p),
             content_type: "b2/x-auto".to_owned(),
+            file_info: None,
         };
 
         if let Some(mime) = args.content_type() {
             mime.clone_into(&mut start_large_file_request.content_type)
         }
 
+        // Set user metadata in file_info.
+        // B2 uses fileInfo field for custom file info in start_large_file API.
+        if let Some(user_metadata) = args.user_metadata() {
+            let file_info: HashMap<String, String> = user_metadata
+                .iter()
+                .map(|(k, v)| (k.to_string(), percent_encode_path(v)))
+                .collect();
+            start_large_file_request.file_info = Some(file_info);
+        }
+
         let req = req.extension(Operation::Write);
 
         let body =
@@ -599,6 +623,11 @@ pub struct StartLargeFileRequest {
     pub bucket_id: String,
     pub file_name: String,
     pub content_type: String,
+    /// Custom file info (user metadata) to store with the file.
+    /// Keys should NOT include the `X-Bz-Info-` prefix.
+    /// Values should be URL-encoded.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub file_info: Option<HashMap<String, String>>,
 }
 
 /// Response of 
[b2_start_large_file](https://www.backblaze.com/apidocs/b2-start-large-file).
@@ -686,6 +715,11 @@ pub struct File {
     pub content_md5: Option<String>,
     pub content_type: Option<String>,
     pub file_name: String,
+    /// Custom file info (user metadata) stored with the file.
+    /// Keys are the original names without the `X-Bz-Info-` prefix.
+    /// Values are URL-encoded when stored, decoded when read.
+    #[serde(default)]
+    pub file_info: HashMap<String, String>,
 }
 
 pub(super) fn parse_file_info(file: &File) -> Metadata {
@@ -705,6 +739,20 @@ pub(super) fn parse_file_info(file: &File) -> Metadata {
         metadata.set_content_type(content_type);
     }
 
+    // Parse user metadata from file_info
+    // B2 stores user metadata with keys stripped of the "X-Bz-Info-" prefix
+    // and values are URL-encoded
+    if !file.file_info.is_empty() {
+        let user_metadata: HashMap<String, String> = file
+            .file_info
+            .iter()
+            .map(|(k, v)| (k.to_lowercase(), percent_decode_path(v)))
+            .collect();
+        if !user_metadata.is_empty() {
+            metadata = metadata.with_user_metadata(user_metadata);
+        }
+    }
+
     metadata
 }
 

Reply via email to