diff --git a/Cargo.lock b/Cargo.lock
index 747e7976..916574ee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1311,6 +1311,16 @@ dependencies = [
"bytes",
]
+[[package]]
+name = "bzip2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
[[package]]
name = "bzip2"
version = "0.6.1"
@@ -1320,6 +1330,16 @@ dependencies = [
"libbz2-rs-sys",
]
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
[[package]]
name = "camino"
version = "1.2.2"
@@ -1584,7 +1604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
dependencies = [
"brotli",
- "bzip2",
+ "bzip2 0.6.1",
"compression-core",
"flate2",
"liblzma",
@@ -2212,7 +2232,7 @@ dependencies = [
"arrow-schema",
"async-trait",
"bytes",
- "bzip2",
+ "bzip2 0.6.1",
"chrono",
"datafusion-catalog",
"datafusion-catalog-listing",
@@ -2350,7 +2370,7 @@ dependencies = [
"async-compression",
"async-trait",
"bytes",
- "bzip2",
+ "bzip2 0.6.1",
"chrono",
"datafusion-common",
"datafusion-common-runtime",
@@ -7729,21 +7749,49 @@ name = "rustfs-protocols"
version = "0.0.5"
dependencies = [
"async-trait",
+ "axum",
+ "base64 0.22.1",
"bytes",
+ "bzip2 0.4.4",
+ "flate2",
+ "futures",
"futures-util",
+ "hex",
+ "hmac 0.13.0-rc.5",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper",
"libunftp",
+ "md5",
+ "percent-encoding",
+ "pin-project-lite",
+ "quick-xml 0.39.2",
+ "rand 0.10.0",
+ "regex",
"rustfs-credentials",
+ "rustfs-ecstore",
"rustfs-iam",
+ "rustfs-keystone",
"rustfs-policy",
+ "rustfs-rio",
"rustfs-utils",
"rustls",
"s3s",
+ "serde",
"serde_json",
+ "sha1 0.11.0-rc.5",
+ "sha2 0.11.0-rc.5",
+ "tar",
"thiserror 2.0.18",
"time",
"tokio",
+ "tokio-util",
+ "tower",
"tracing",
"unftp-core",
+ "urlencoding",
+ "uuid",
]
[[package]]
@@ -9094,6 +9142,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
+[[package]]
+name = "tar"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
[[package]]
name = "temp-env"
version = "0.3.6"
@@ -10718,7 +10777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004"
dependencies = [
"aes 0.8.4",
- "bzip2",
+ "bzip2 0.6.1",
"constant_time_eq",
"crc32fast",
"deflate64",
diff --git a/Cargo.toml b/Cargo.toml
index 6ceae3a7..e1459fcc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -204,6 +204,7 @@ glob = "0.3.3"
google-cloud-storage = "1.8.0"
google-cloud-auth = "1.6.0"
hashbrown = { version = "0.16.1", features = ["serde", "rayon"] }
+hex = "0.4.3"
hex-simd = "0.8.0"
highway = { version = "1.3.0" }
ipnetwork = { version = "0.21.1", features = ["serde"] }
@@ -224,6 +225,7 @@ object_store = "0.12.5"
parking_lot = "0.12.5"
path-absolutize = "3.1.1"
path-clean = "1.0.1"
+percent-encoding = "2.3.2"
pin-project-lite = "0.2.17"
pretty_assertions = "1.4.1"
rand = { version = "0.10.0", features = ["serde"] }
diff --git a/README.md b/README.md
index 33f43bd5..2689c2e6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[](https://rustfs.com)
+[](https://rustfs.com)
RustFS is a high-performance, distributed object storage system built in Rust.
@@ -42,6 +42,7 @@ Unlike other storage systems, RustFS is released under the permissible Apache 2.
- **High Performance**: Built with Rust to ensure maximum speed and resource efficiency.
- **Distributed Architecture**: Scalable and fault-tolerant design suitable for large-scale deployments.
- **S3 Compatibility**: Seamless integration with existing S3-compatible applications and tools.
+- **OpenStack Swift API**: Native support for Swift protocol with Keystone authentication.
- **OpenStack Keystone Integration**: Native support for OpenStack Keystone authentication with X-Auth-Token headers.
- **Data Lake Support**: Optimized for high-throughput big data and AI workloads.
- **Open Source**: Licensed under Apache 2.0, encouraging unrestricted community contributions and commercial usage.
@@ -56,6 +57,7 @@ Unlike other storage systems, RustFS is released under the permissible Apache 2.
| **Event Notifications** | ✅ Available | **Distributed Mode** | 🚧 Under Testing |
| **K8s Helm Charts** | ✅ Available | **RustFS KMS** | 🚧 Under Testing |
| **Keystone Auth** | ✅ Available | **Multi-Tenancy** | ✅ Available |
+| **Swift API** | ✅ Available | **Swift Metadata Ops** | 🚧 Partial |
## RustFS vs MinIO Performance
diff --git a/_typos.toml b/_typos.toml
index 95ee27bd..2d1aa7e5 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -42,6 +42,8 @@ GAE = "GAE"
# s3-tests original test names (cannot be changed)
nonexisted = "nonexisted"
consts = "consts"
+# Swift API - company/product names
+Hashi = "Hashi" # HashiCorp
[files]
extend-exclude = []
diff --git a/crates/protocols/Cargo.toml b/crates/protocols/Cargo.toml
index a4e2b3cf..9788065e 100644
--- a/crates/protocols/Cargo.toml
+++ b/crates/protocols/Cargo.toml
@@ -28,6 +28,36 @@ categories = ["network-programming", "filesystem"]
[features]
default = []
ftps = ["dep:libunftp", "dep:unftp-core", "dep:rustls"]
+swift = [
+ "dep:rustfs-keystone",
+ "dep:rustfs-ecstore",
+ "dep:rustfs-rio",
+ "dep:axum",
+ "dep:http",
+ "dep:hyper",
+ "dep:tower",
+ "dep:regex",
+ "dep:percent-encoding",
+ "dep:sha2",
+ "dep:uuid",
+ "dep:pin-project-lite",
+ "dep:futures",
+ "dep:http-body",
+ "dep:http-body-util",
+ "dep:tokio-util",
+ "dep:serde",
+ "dep:urlencoding",
+ "dep:md5",
+ "dep:quick-xml",
+ "dep:hmac",
+ "dep:sha1",
+ "dep:hex",
+ "dep:tar",
+ "dep:flate2",
+ "dep:bzip2",
+ "dep:base64",
+ "dep:rand",
+]
[dependencies]
# Core RustFS dependencies
@@ -60,6 +90,36 @@ libunftp = { workspace = true, optional = true }
unftp-core = { workspace = true, optional = true }
rustls = { workspace = true, optional = true }
+# Swift specific dependencies (optional)
+rustfs-keystone = { workspace = true, optional = true }
+rustfs-ecstore = { workspace = true, optional = true }
+rustfs-rio = { workspace = true, optional = true }
+axum = { workspace = true, optional = true }
+http = { workspace = true, optional = true }
+hyper = { workspace = true, optional = true }
+tower = { workspace = true, optional = true }
+regex = { workspace = true, optional = true }
+percent-encoding = { workspace = true, optional = true }
+sha2 = { workspace = true, optional = true }
+uuid = { workspace = true, optional = true }
+pin-project-lite = { workspace = true, optional = true }
+futures = { workspace = true, optional = true }
+http-body = { workspace = true, optional = true }
+http-body-util = { workspace = true, optional = true }
+tokio-util = { workspace = true, optional = true }
+serde = { workspace = true, optional = true }
+urlencoding = { workspace = true, optional = true }
+md5 = { workspace = true, optional = true }
+quick-xml = { workspace = true, optional = true, features = ["serialize"] }
+hmac = { workspace = true, optional = true }
+sha1 = { workspace = true, optional = true }
+hex = { workspace = true, optional = true }
+tar = { version = "0.4", optional = true }
+flate2 = { workspace = true, optional = true }
+bzip2 = { version = "0.4", optional = true }
+base64 = { workspace = true, optional = true }
+rand = { workspace = true, optional = true }
+
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
diff --git a/crates/protocols/src/common/gateway.rs b/crates/protocols/src/common/gateway.rs
index b28ea9c7..12d2bff0 100644
--- a/crates/protocols/src/common/gateway.rs
+++ b/crates/protocols/src/common/gateway.rs
@@ -155,6 +155,33 @@ pub fn is_operation_supported(protocol: super::session::Protocol, action: &S3Act
S3Action::ListBuckets => true, // LIST at root level
S3Action::HeadBucket => true, // Can check if directory exists
},
+ super::session::Protocol::Swift => match action {
+ // Swift supports most S3 operations via translation
+ S3Action::CreateBucket => true, // PUT container
+ S3Action::DeleteBucket => true, // DELETE container
+ S3Action::GetObject => true, // GET object
+ S3Action::PutObject => true, // PUT object
+ S3Action::DeleteObject => true, // DELETE object
+ S3Action::HeadObject => true, // HEAD object
+ S3Action::CopyObject => true, // COPY method
+ S3Action::ListBucket => true, // GET container
+ S3Action::ListBuckets => true, // GET account
+ S3Action::HeadBucket => true, // HEAD container
+
+ // Multipart not directly supported by Swift API (uses different approach)
+ S3Action::CreateMultipartUpload => false,
+ S3Action::UploadPart => false,
+ S3Action::CompleteMultipartUpload => false,
+ S3Action::AbortMultipartUpload => false,
+ S3Action::ListMultipartUploads => false,
+ S3Action::ListParts => false,
+
+ // ACL operations not supported by Swift API (uses different model)
+ S3Action::GetBucketAcl => false,
+ S3Action::PutBucketAcl => false,
+ S3Action::GetObjectAcl => false,
+ S3Action::PutObjectAcl => false,
+ },
}
}
diff --git a/crates/protocols/src/common/session.rs b/crates/protocols/src/common/session.rs
index e7a44d63..8aca7e27 100644
--- a/crates/protocols/src/common/session.rs
+++ b/crates/protocols/src/common/session.rs
@@ -20,6 +20,7 @@ use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Protocol {
Ftps,
+ Swift,
}
/// Protocol principal representing an authenticated user
diff --git a/crates/protocols/src/lib.rs b/crates/protocols/src/lib.rs
index 1dee33ec..10eb43a7 100644
--- a/crates/protocols/src/lib.rs
+++ b/crates/protocols/src/lib.rs
@@ -20,8 +20,14 @@ pub mod constants;
#[cfg(feature = "ftps")]
pub mod ftps;
+#[cfg(feature = "swift")]
+pub mod swift;
+
pub use common::session::Protocol;
pub use common::{AuthorizationError, ProtocolPrincipal, S3Action, SessionContext, authorize_operation};
#[cfg(feature = "ftps")]
pub use ftps::{config::FtpsConfig, server::FtpsServer};
+
+#[cfg(feature = "swift")]
+pub use swift::handler::SwiftService;
diff --git a/crates/protocols/src/swift/README.md b/crates/protocols/src/swift/README.md
new file mode 100644
index 00000000..bc56fa8c
--- /dev/null
+++ b/crates/protocols/src/swift/README.md
@@ -0,0 +1,134 @@
+# OpenStack Swift API for RustFS
+
+Swift-compatible object storage API implementation for RustFS.
+
+## Features
+
+This implementation provides **Phase 1 Swift API support** (~25% of full Swift API):
+
+- ✅ Container CRUD operations (create, list, delete, metadata)
+- ✅ Object CRUD with streaming downloads (upload, get, head, delete)
+- ✅ Keystone token authentication
+- ✅ Multi-tenant isolation with secure SHA256-based bucket prefixing
+- ✅ Server-side object copy (COPY method)
+- ✅ HTTP Range requests for partial downloads (206, 416 responses)
+- ✅ Custom metadata support (X-Object-Meta-*, X-Container-Meta-*)
+
+**Not yet implemented:**
+- ⏳ Account-level operations (statistics, metadata)
+- ⏳ Large object support (multi-part uploads >5GB)
+- ⏳ Object versioning
+- ⏳ Container ACLs and CORS
+- ⏳ Temporary URLs (TempURL)
+- ⏳ XML/plain-text response formats (JSON only)
+
+## Enable Feature
+
+**Swift API is opt-in and must be explicitly enabled.**
+
+Build with Swift support:
+
+```bash
+cargo build --features swift
+```
+
+Or enable all protocol features:
+
+```bash
+cargo build --features full
+```
+
+**Note:** Swift is NOT enabled by default to avoid unexpected API surface changes in existing deployments.
+
+## Configuration
+
+Swift API uses Keystone for authentication. Configure the following environment variables:
+
+| Variable | Description |
+|----------|-------------|
+| `RUSTFS_KEYSTONE_URL` | Keystone authentication endpoint URL |
+| `RUSTFS_KEYSTONE_ADMIN_TENANT` | Admin tenant/project name |
+| `RUSTFS_KEYSTONE_ADMIN_USER` | Admin username |
+| `RUSTFS_KEYSTONE_ADMIN_PASSWORD` | Admin password |
+
+## API Endpoints
+
+Swift API endpoints follow the pattern: `/v1/AUTH_{project_id}/...`
+
+### Account Operations
+- `GET /v1/AUTH_{project}` - List containers
+- `HEAD /v1/AUTH_{project}` - Get account metadata (not yet implemented)
+- `POST /v1/AUTH_{project}` - Update account metadata (not yet implemented)
+
+### Container Operations
+- `PUT /v1/AUTH_{project}/{container}` - Create container
+- `GET /v1/AUTH_{project}/{container}` - List objects
+- `HEAD /v1/AUTH_{project}/{container}` - Get container metadata
+- `POST /v1/AUTH_{project}/{container}` - Update container metadata
+- `DELETE /v1/AUTH_{project}/{container}` - Delete container
+
+### Object Operations
+- `PUT /v1/AUTH_{project}/{container}/{object}` - Upload object
+- `GET /v1/AUTH_{project}/{container}/{object}` - Download object
+- `HEAD /v1/AUTH_{project}/{container}/{object}` - Get object metadata
+- `POST /v1/AUTH_{project}/{container}/{object}` - Update object metadata
+- `DELETE /v1/AUTH_{project}/{container}/{object}` - Delete object
+- `COPY /v1/AUTH_{project}/{container}/{object}` - Server-side copy
+
+## Architecture
+
+The Swift API is implemented as a Tower service layer (`SwiftService`) that wraps the S3 service:
+
+```
+HTTP Request
+ │
+ ▼
+┌───────────────┐
+│ SwiftService │ ← Routes /v1/AUTH_* requests
+└───────┬───────┘
+ │
+ ┌────┴────┐
+ │ │
+ ▼ ▼
+Swift S3 Service
+Handler (fallback)
+```
+
+### Key Components
+
+- **handler.rs** - Main service implementing Tower's Service trait
+- **router.rs** - URL routing and parsing for Swift paths
+- **container.rs** - Container operations with tenant isolation
+- **object.rs** - Object operations including copy and range requests
+- **account.rs** - Account validation and tenant access control
+- **errors.rs** - Swift-specific error types
+- **types.rs** - Data structures for Swift API responses
+
+### Tenant Isolation
+
+Swift containers are mapped to S3 buckets with a secure hash prefix:
+
+```
+Swift: /v1/AUTH_abc123/mycontainer
+ ↓
+S3 Bucket: {sha256(abc123)[0:16]}-mycontainer
+```
+
+This ensures:
+- Complete tenant isolation at the storage layer
+- No collision between tenants with similar container names
+- S3-compatible bucket naming (lowercase alphanumeric + hyphen)
+
+## Documentation
+
+See the `docs/` directory for detailed documentation:
+
+- `SWIFT_API.md` - Complete API reference
+- `TESTING_GUIDE.md` - Manual testing procedures
+- `COMPLETION_ANALYSIS.md` - Protocol coverage tracking
+- `COPY_IMPLEMENTATION.md` - Server-side copy documentation
+- `RANGE_REQUESTS.md` - Range request implementation details
+
+## License
+
+Apache License 2.0
diff --git a/crates/protocols/src/swift/account.rs b/crates/protocols/src/swift/account.rs
new file mode 100644
index 00000000..ce2e9d60
--- /dev/null
+++ b/crates/protocols/src/swift/account.rs
@@ -0,0 +1,348 @@
+// Copyright 2024 RustFS Team
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Swift account operations and validation
+
+use super::{SwiftError, SwiftResult};
+use rustfs_credentials::Credentials;
+use rustfs_ecstore::new_object_layer_fn;
+use rustfs_ecstore::store_api::{BucketOperations, MakeBucketOptions};
+use s3s::dto::{Tag, Tagging};
+use sha2::{Digest, Sha256};
+use std::collections::HashMap;
+use time;
+
+/// Validate that the authenticated user has access to the requested account
+///
+/// This function ensures tenant isolation by verifying that the account
+/// in the URL matches the project_id from the Keystone credentials.
+///
+/// # Arguments
+///
+/// * `account` - Account identifier from URL (e.g., "AUTH_7188e165...")
+/// * `credentials` - Keystone credentials from middleware
+///
+/// # Returns
+///
+/// The project_id if validation succeeds, or an error if:
+/// - Account format is invalid
+/// - Credentials don't contain project_id
+/// - Account project_id doesn't match credentials project_id
+#[allow(dead_code)] // Used by Swift implementation
+pub fn validate_account_access(account: &str, credentials: &Credentials) -> SwiftResult {
+ // Extract project_id from account (strip "AUTH_" prefix)
+ let account_project_id = account
+ .strip_prefix("AUTH_")
+ .ok_or_else(|| SwiftError::BadRequest(format!("Invalid account format: {}. Expected AUTH_{{project_id}}", account)))?;
+
+ // Get project_id from Keystone credentials
+ let cred_project_id = credentials
+ .claims
+ .as_ref()
+ .and_then(|claims| claims.get("keystone_project_id"))
+ .and_then(|v| v.as_str())
+ .ok_or_else(|| {
+ SwiftError::Unauthorized("Missing project_id in credentials. Keystone authentication required.".to_string())
+ })?;
+
+ // Verify account matches authenticated project
+ if account_project_id != cred_project_id {
+ return Err(SwiftError::Forbidden(format!(
+ "Access denied. Account {} does not match authenticated project {}",
+ account_project_id, cred_project_id
+ )));
+ }
+
+ Ok(cred_project_id.to_string())
+}
+
+/// Check if user has admin privileges
+///
+/// Admin users (with "admin" or "reseller_admin" roles) can perform
+/// cross-tenant operations and administrative tasks.
+#[allow(dead_code)] // Used by Swift implementation
+pub fn is_admin_user(credentials: &Credentials) -> bool {
+ credentials
+ .claims
+ .as_ref()
+ .and_then(|claims| claims.get("keystone_roles"))
+ .and_then(|roles| roles.as_array())
+ .map(|roles| {
+ roles
+ .iter()
+ .any(|r| r.as_str().map(|s| s == "admin" || s == "reseller_admin").unwrap_or(false))
+ })
+ .unwrap_or(false)
+}
+
+/// Get account metadata bucket name
+///
+/// Account metadata is stored in a special S3 bucket named after
+/// the hashed account identifier. This allows storing TempURL keys
+/// and other account-level metadata.
+///
+/// # Format
+/// ```text
+/// swift-account-{sha256(account)[0..16]}
+/// ```
+fn get_account_metadata_bucket_name(account: &str) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(account.as_bytes());
+ let hash_bytes = hasher.finalize();
+ let hash = hex::encode(hash_bytes);
+ format!("swift-account-{}", &hash[0..16])
+}
+
+/// Get account metadata from S3 bucket tags
+///
+/// Retrieves account-level metadata such as TempURL keys.
+/// Metadata is stored as S3 bucket tags with the prefix `swift-account-meta-`.
+///
+/// # Arguments
+/// * `account` - Account identifier (e.g., "AUTH_7188e165...")
+/// * `credentials` - S3 credentials for accessing the metadata bucket
+///
+/// # Returns
+/// HashMap of metadata key-value pairs (without the prefix)
+pub async fn get_account_metadata(account: &str, _credentials: &Option) -> SwiftResult> {
+ let bucket_name = get_account_metadata_bucket_name(account);
+
+ // Try to load bucket metadata
+ let bucket_meta = match rustfs_ecstore::bucket::metadata_sys::get(&bucket_name).await {
+ Ok(meta) => meta,
+ Err(_) => {
+ // Bucket doesn't exist - return empty metadata
+ return Ok(HashMap::new());
+ }
+ };
+
+ // Extract metadata from bucket tags
+ let mut metadata = HashMap::new();
+ if let Some(tagging) = &bucket_meta.tagging_config {
+ for tag in &tagging.tag_set {
+ if let (Some(key), Some(value)) = (&tag.key, &tag.value)
+ && let Some(meta_key) = key.strip_prefix("swift-account-meta-")
+ {
+ // Strip "swift-account-meta-" prefix
+ metadata.insert(meta_key.to_string(), value.clone());
+ }
+ }
+ }
+
+ Ok(metadata)
+}
+
+/// Update account metadata (stored in S3 bucket tags)
+///
+/// Updates account-level metadata such as TempURL keys.
+/// Only updates swift-account-meta-* tags, preserving other tags.
+///
+/// # Arguments
+/// * `account` - Account identifier
+/// * `metadata` - Metadata key-value pairs to store (keys will be prefixed with `swift-account-meta-`)
+/// * `credentials` - S3 credentials
+pub async fn update_account_metadata(
+ account: &str,
+ metadata: &HashMap,
+ _credentials: &Option,
+) -> SwiftResult<()> {
+ let bucket_name = get_account_metadata_bucket_name(account);
+
+ let Some(store) = new_object_layer_fn() else {
+ return Err(SwiftError::InternalServerError("Storage layer not initialized".to_string()));
+ };
+
+ // Create bucket if it doesn't exist
+ let bucket_exists = rustfs_ecstore::bucket::metadata_sys::get(&bucket_name).await.is_ok();
+ if !bucket_exists {
+ // Create bucket for account metadata
+ store
+ .make_bucket(&bucket_name, &MakeBucketOptions::default())
+ .await
+ .map_err(|e| SwiftError::InternalServerError(format!("Failed to create account metadata bucket: {}", e)))?;
+ }
+
+ // Load current bucket metadata
+ let bucket_meta = rustfs_ecstore::bucket::metadata_sys::get(&bucket_name)
+ .await
+ .map_err(|e| SwiftError::InternalServerError(format!("Failed to load bucket metadata: {}", e)))?;
+
+ let mut bucket_meta_clone = (*bucket_meta).clone();
+
+ // Get existing tags, preserving non-Swift tags
+ let mut existing_tagging = bucket_meta_clone
+ .tagging_config
+ .clone()
+ .unwrap_or_else(|| Tagging { tag_set: vec![] });
+
+ // Remove old swift-account-meta-* tags while preserving other tags
+ existing_tagging.tag_set.retain(|tag| {
+ if let Some(key) = &tag.key {
+ !key.starts_with("swift-account-meta-")
+ } else {
+ true
+ }
+ });
+
+ // Add new metadata tags
+ for (key, value) in metadata {
+ existing_tagging.tag_set.push(Tag {
+ key: Some(format!("swift-account-meta-{}", key)),
+ value: Some(value.clone()),
+ });
+ }
+
+ let now = time::OffsetDateTime::now_utc();
+
+ if existing_tagging.tag_set.is_empty() {
+ // No tags remain; clear tagging config
+ bucket_meta_clone.tagging_config_xml = Vec::new();
+ bucket_meta_clone.tagging_config_updated_at = now;
+ bucket_meta_clone.tagging_config = None;
+ } else {
+ // Serialize tags to XML
+ let tagging_xml = quick_xml::se::to_string(&existing_tagging)
+ .map_err(|e| SwiftError::InternalServerError(format!("Failed to serialize tags: {}", e)))?;
+
+ bucket_meta_clone.tagging_config_xml = tagging_xml.into_bytes();
+ bucket_meta_clone.tagging_config_updated_at = now;
+ bucket_meta_clone.tagging_config = Some(existing_tagging);
+ }
+
+ // Save updated metadata
+ rustfs_ecstore::bucket::metadata_sys::set_bucket_metadata(bucket_name.clone(), bucket_meta_clone)
+ .await
+ .map_err(|e| SwiftError::InternalServerError(format!("Failed to save metadata: {}", e)))?;
+
+ Ok(())
+}
+
+/// Get TempURL key for account
+///
+/// Retrieves the TempURL key from account metadata.
+/// Returns None if no TempURL key is set.
+pub async fn get_tempurl_key(account: &str, credentials: &Option) -> SwiftResult