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
}