feat(bucket-tenants): add persisted bucket-scoped tenant credentials with bucket export and import APIs
This commit is contained in:
+255
-64
@@ -10,8 +10,8 @@ use hyper_util::rt::TokioIo;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::net::TcpListener;
|
||||
@@ -21,9 +21,6 @@ use uuid::Uuid;
|
||||
|
||||
use crate::action::{self, RequestContext, StorageAction};
|
||||
use crate::auth::{self, AuthenticatedIdentity};
|
||||
use crate::config::SmartStorageConfig;
|
||||
use crate::policy::{self, PolicyDecision, PolicyStore};
|
||||
use crate::error::StorageError;
|
||||
use crate::cluster::coordinator::DistributedStore;
|
||||
use crate::cluster::drive_manager::DriveManager;
|
||||
use crate::cluster::healing::HealingService;
|
||||
@@ -34,6 +31,9 @@ use crate::cluster::protocol::NodeInfo;
|
||||
use crate::cluster::quic_transport::QuicTransport;
|
||||
use crate::cluster::shard_store::ShardStore;
|
||||
use crate::cluster::state::ClusterState;
|
||||
use crate::config::{Credential, SmartStorageConfig};
|
||||
use crate::error::StorageError;
|
||||
use crate::policy::{self, PolicyDecision, PolicyStore};
|
||||
use crate::storage::{FileStore, StorageBackend};
|
||||
use crate::xml_response;
|
||||
|
||||
@@ -70,7 +70,6 @@ pub struct StorageServer {
|
||||
|
||||
impl StorageServer {
|
||||
pub async fn start(config: SmartStorageConfig) -> Result<Self> {
|
||||
let auth_runtime = Arc::new(auth::RuntimeCredentialStore::new(&config.auth));
|
||||
let mut cluster_shutdown_txs = Vec::new();
|
||||
let store: Arc<StorageBackend> = if let Some(ref cluster_config) = config.cluster {
|
||||
if cluster_config.enabled {
|
||||
@@ -88,8 +87,12 @@ impl StorageServer {
|
||||
let policy_store = Arc::new(PolicyStore::new(store.policies_dir()));
|
||||
policy_store.load_from_disk().await?;
|
||||
|
||||
let addr: SocketAddr = format!("{}:{}", config.address(), config.server.port)
|
||||
.parse()?;
|
||||
let auth_runtime = Arc::new(
|
||||
auth::RuntimeCredentialStore::new(&config.auth, Some(Self::credentials_path(&config)))
|
||||
.await?,
|
||||
);
|
||||
|
||||
let addr: SocketAddr = format!("{}:{}", config.address(), config.server.port).parse()?;
|
||||
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||
@@ -181,15 +184,81 @@ impl StorageServer {
|
||||
|
||||
pub async fn replace_credentials(
|
||||
&self,
|
||||
credentials: Vec<crate::config::Credential>,
|
||||
credentials: Vec<Credential>,
|
||||
) -> Result<(), StorageError> {
|
||||
self.auth_runtime.replace_credentials(credentials).await
|
||||
}
|
||||
|
||||
pub async fn create_bucket_tenant(
|
||||
&self,
|
||||
bucket_name: &str,
|
||||
credential: Credential,
|
||||
) -> Result<Credential> {
|
||||
self.ensure_tenant_auth_enabled()?;
|
||||
self.store.create_bucket(bucket_name).await?;
|
||||
Ok(self
|
||||
.auth_runtime
|
||||
.replace_bucket_tenant_credential(bucket_name, credential)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn rotate_bucket_tenant_credentials(
|
||||
&self,
|
||||
bucket_name: &str,
|
||||
credential: Credential,
|
||||
) -> Result<Credential> {
|
||||
self.ensure_tenant_auth_enabled()?;
|
||||
if !self.store.bucket_exists(bucket_name).await {
|
||||
return Err(StorageError::no_such_bucket().into());
|
||||
}
|
||||
Ok(self
|
||||
.auth_runtime
|
||||
.replace_bucket_tenant_credential(bucket_name, credential)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn delete_bucket_tenant(
|
||||
&self,
|
||||
bucket_name: &str,
|
||||
access_key_id: Option<&str>,
|
||||
) -> Result<()> {
|
||||
self.ensure_tenant_auth_enabled()?;
|
||||
self.auth_runtime
|
||||
.remove_bucket_tenant_credentials(bucket_name, access_key_id)
|
||||
.await?;
|
||||
if access_key_id.is_none() && self.store.bucket_exists(bucket_name).await {
|
||||
self.store.delete_bucket_recursive(bucket_name).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_bucket_tenants(&self) -> Vec<crate::auth::BucketTenantMetadata> {
|
||||
self.auth_runtime.list_bucket_tenants().await
|
||||
}
|
||||
|
||||
pub async fn get_bucket_tenant_credential(&self, bucket_name: &str) -> Option<Credential> {
|
||||
self.auth_runtime
|
||||
.get_bucket_tenant_credential(bucket_name)
|
||||
.await
|
||||
}
|
||||
|
||||
fn ensure_tenant_auth_enabled(&self) -> Result<()> {
|
||||
if !self.auth_runtime.enabled() {
|
||||
anyhow::bail!("Bucket tenants require auth.enabled=true");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn credentials_path(config: &SmartStorageConfig) -> std::path::PathBuf {
|
||||
std::path::PathBuf::from(&config.storage.directory)
|
||||
.join(".smartstorage")
|
||||
.join("credentials.json")
|
||||
}
|
||||
|
||||
async fn start_standalone(config: &SmartStorageConfig) -> Result<Arc<StorageBackend>> {
|
||||
let store = Arc::new(StorageBackend::Standalone(
|
||||
FileStore::new(config.storage.directory.clone().into()),
|
||||
));
|
||||
let store = Arc::new(StorageBackend::Standalone(FileStore::new(
|
||||
config.storage.directory.clone().into(),
|
||||
)));
|
||||
if config.storage.clean_slate {
|
||||
store.reset().await?;
|
||||
} else {
|
||||
@@ -208,7 +277,9 @@ impl StorageServer {
|
||||
let topology_path = persistence::topology_path(&cluster_metadata_dir);
|
||||
let persisted_identity = persistence::load_identity(&identity_path).await?;
|
||||
|
||||
if let (Some(configured_node_id), Some(identity)) = (&cluster_config.node_id, &persisted_identity) {
|
||||
if let (Some(configured_node_id), Some(identity)) =
|
||||
(&cluster_config.node_id, &persisted_identity)
|
||||
{
|
||||
if configured_node_id != &identity.node_id {
|
||||
anyhow::bail!(
|
||||
"Configured cluster node ID '{}' conflicts with persisted node ID '{}'",
|
||||
@@ -221,7 +292,11 @@ impl StorageServer {
|
||||
let node_id = cluster_config
|
||||
.node_id
|
||||
.clone()
|
||||
.or_else(|| persisted_identity.as_ref().map(|identity| identity.node_id.clone()))
|
||||
.or_else(|| {
|
||||
persisted_identity
|
||||
.as_ref()
|
||||
.map(|identity| identity.node_id.clone())
|
||||
})
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
let cluster_id = persisted_identity
|
||||
.as_ref()
|
||||
@@ -273,7 +348,9 @@ impl StorageServer {
|
||||
let has_persisted_topology = persisted_topology.is_some();
|
||||
if let Some(topology) = persisted_topology {
|
||||
if topology.cluster_id != cluster_id {
|
||||
anyhow::bail!("Persisted topology cluster ID does not match persisted node identity");
|
||||
anyhow::bail!(
|
||||
"Persisted topology cluster ID does not match persisted node identity"
|
||||
);
|
||||
}
|
||||
cluster_state.apply_topology(&topology).await;
|
||||
} else if cluster_config.seed_nodes.is_empty() {
|
||||
@@ -347,7 +424,11 @@ impl StorageServer {
|
||||
let shard_stores_for_accept = local_shard_stores.clone();
|
||||
tokio::spawn(async move {
|
||||
transport_clone
|
||||
.accept_loop(shard_stores_for_accept, Some(cluster_state_for_accept), quic_shutdown_rx)
|
||||
.accept_loop(
|
||||
shard_stores_for_accept,
|
||||
Some(cluster_state_for_accept),
|
||||
quic_shutdown_rx,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
@@ -400,7 +481,10 @@ impl StorageServer {
|
||||
);
|
||||
}
|
||||
|
||||
Ok((store, vec![quic_shutdown_tx, hb_shutdown_tx, heal_shutdown_tx]))
|
||||
Ok((
|
||||
store,
|
||||
vec![quic_shutdown_tx, hb_shutdown_tx, heal_shutdown_tx],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,17 +498,26 @@ impl SmartStorageConfig {
|
||||
// Request handling
|
||||
// ============================
|
||||
|
||||
type BoxBody = http_body_util::combinators::BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
|
||||
type BoxBody =
|
||||
http_body_util::combinators::BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
fn full_body(data: impl Into<Bytes>) -> BoxBody {
|
||||
http_body_util::Full::new(data.into())
|
||||
.map_err(|never: std::convert::Infallible| -> Box<dyn std::error::Error + Send + Sync> { match never {} })
|
||||
.map_err(
|
||||
|never: std::convert::Infallible| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
match never {}
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn empty_body() -> BoxBody {
|
||||
http_body_util::Empty::new()
|
||||
.map_err(|never: std::convert::Infallible| -> Box<dyn std::error::Error + Send + Sync> { match never {} })
|
||||
.map_err(
|
||||
|never: std::convert::Infallible| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
match never {}
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -445,10 +538,10 @@ impl Stream for FrameStream {
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let inner = unsafe { self.map_unchecked_mut(|s| &mut s.inner) };
|
||||
match inner.poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(bytes))) => {
|
||||
Poll::Ready(Some(Ok(hyper::body::Frame::data(bytes))))
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>))),
|
||||
Poll::Ready(Some(Ok(bytes))) => Poll::Ready(Some(Ok(hyper::body::Frame::data(bytes)))),
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(
|
||||
Box::new(e) as Box<dyn std::error::Error + Send + Sync>
|
||||
))),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
@@ -482,7 +575,11 @@ fn storage_error_response(err: &StorageError, request_id: &str) -> Response<BoxB
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn json_response(status: StatusCode, value: serde_json::Value, request_id: &str) -> Response<BoxBody> {
|
||||
fn json_response(
|
||||
status: StatusCode,
|
||||
value: serde_json::Value,
|
||||
request_id: &str,
|
||||
) -> Response<BoxBody> {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header("content-type", "application/json")
|
||||
@@ -491,7 +588,12 @@ fn json_response(status: StatusCode, value: serde_json::Value, request_id: &str)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn text_response(status: StatusCode, content_type: &str, body: String, request_id: &str) -> Response<BoxBody> {
|
||||
fn text_response(
|
||||
status: StatusCode,
|
||||
content_type: &str,
|
||||
body: String,
|
||||
request_id: &str,
|
||||
) -> Response<BoxBody> {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header("content-type", content_type)
|
||||
@@ -521,17 +623,20 @@ async fn handle_request(
|
||||
}
|
||||
|
||||
if method == Method::GET && uri.path().starts_with("/-/") {
|
||||
let resp = match handle_operational_request(uri.path(), store, &config, &metrics, &request_id).await {
|
||||
Ok(resp) => resp,
|
||||
Err(error) => {
|
||||
tracing::error!(error = %error, "Operational endpoint failed");
|
||||
json_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
serde_json::json!({ "ok": false, "error": error.to_string() }),
|
||||
&request_id,
|
||||
)
|
||||
}
|
||||
};
|
||||
let resp =
|
||||
match handle_operational_request(uri.path(), store, &config, &metrics, &request_id)
|
||||
.await
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
Err(error) => {
|
||||
tracing::error!(error = %error, "Operational endpoint failed");
|
||||
json_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
serde_json::json!({ "ok": false, "error": error.to_string() }),
|
||||
&request_id,
|
||||
)
|
||||
}
|
||||
};
|
||||
metrics.record_response(resp.status());
|
||||
return Ok(resp);
|
||||
}
|
||||
@@ -672,7 +777,11 @@ async fn handle_operational_request(
|
||||
let cluster_health = store.get_cluster_health().await?;
|
||||
let stats = store.get_storage_stats().await?;
|
||||
let cluster_enabled = if cluster_health.enabled { 1 } else { 0 };
|
||||
let quorum_healthy = if cluster_health.quorum_healthy.unwrap_or(true) { 1 } else { 0 };
|
||||
let quorum_healthy = if cluster_health.quorum_healthy.unwrap_or(true) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let body = format!(
|
||||
"# HELP smartstorage_requests_total Total HTTP requests observed by smartstorage.\n\
|
||||
# TYPE smartstorage_requests_total counter\n\
|
||||
@@ -720,6 +829,12 @@ async fn authorize_request(
|
||||
identity: Option<&AuthenticatedIdentity>,
|
||||
policy_store: &PolicyStore,
|
||||
) -> Result<(), StorageError> {
|
||||
if let Some(identity) = identity {
|
||||
if let Some(bucket_name) = identity.bucket_name.as_deref() {
|
||||
authorize_scoped_credential(ctx, bucket_name)?;
|
||||
}
|
||||
}
|
||||
|
||||
// ListAllMyBuckets requires authentication (no bucket to apply policy to)
|
||||
if ctx.action == StorageAction::ListAllMyBuckets {
|
||||
if identity.is_none() {
|
||||
@@ -750,6 +865,46 @@ async fn authorize_request(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn authorize_scoped_credential(
|
||||
ctx: &RequestContext,
|
||||
bucket_name: &str,
|
||||
) -> Result<(), StorageError> {
|
||||
let Some(request_bucket) = ctx.bucket.as_deref() else {
|
||||
return Err(StorageError::access_denied());
|
||||
};
|
||||
|
||||
if request_bucket != bucket_name {
|
||||
return Err(StorageError::access_denied());
|
||||
}
|
||||
|
||||
if let Some(source_bucket) = ctx.source_bucket.as_deref() {
|
||||
if source_bucket != bucket_name {
|
||||
return Err(StorageError::access_denied());
|
||||
}
|
||||
}
|
||||
|
||||
match ctx.action {
|
||||
StorageAction::CreateBucket
|
||||
| StorageAction::DeleteBucket
|
||||
| StorageAction::GetBucketPolicy
|
||||
| StorageAction::PutBucketPolicy
|
||||
| StorageAction::DeleteBucketPolicy
|
||||
| StorageAction::ListAllMyBuckets => Err(StorageError::access_denied()),
|
||||
StorageAction::HeadBucket
|
||||
| StorageAction::ListBucket
|
||||
| StorageAction::GetObject
|
||||
| StorageAction::HeadObject
|
||||
| StorageAction::PutObject
|
||||
| StorageAction::DeleteObject
|
||||
| StorageAction::CopyObject
|
||||
| StorageAction::ListBucketMultipartUploads
|
||||
| StorageAction::AbortMultipartUpload
|
||||
| StorageAction::InitiateMultipartUpload
|
||||
| StorageAction::UploadPart
|
||||
| StorageAction::CompleteMultipartUpload => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Routing
|
||||
// ============================
|
||||
@@ -788,9 +943,16 @@ async fn route_request(
|
||||
// Check for ?policy query parameter
|
||||
if query.contains_key("policy") {
|
||||
return match method {
|
||||
Method::GET => handle_get_bucket_policy(policy_store, &bucket, request_id).await,
|
||||
Method::PUT => handle_put_bucket_policy(req, &store, policy_store, &bucket, request_id).await,
|
||||
Method::DELETE => handle_delete_bucket_policy(policy_store, &bucket, request_id).await,
|
||||
Method::GET => {
|
||||
handle_get_bucket_policy(policy_store, &bucket, request_id).await
|
||||
}
|
||||
Method::PUT => {
|
||||
handle_put_bucket_policy(req, &store, policy_store, &bucket, request_id)
|
||||
.await
|
||||
}
|
||||
Method::DELETE => {
|
||||
handle_delete_bucket_policy(policy_store, &bucket, request_id).await
|
||||
}
|
||||
_ => Ok(empty_response(StatusCode::METHOD_NOT_ALLOWED, request_id)),
|
||||
};
|
||||
}
|
||||
@@ -804,7 +966,9 @@ async fn route_request(
|
||||
}
|
||||
}
|
||||
Method::PUT => handle_create_bucket(store, &bucket, request_id).await,
|
||||
Method::DELETE => handle_delete_bucket(store, &bucket, request_id, policy_store).await,
|
||||
Method::DELETE => {
|
||||
handle_delete_bucket(store, &bucket, request_id, policy_store).await
|
||||
}
|
||||
Method::HEAD => handle_head_bucket(store, &bucket, request_id).await,
|
||||
_ => Ok(empty_response(StatusCode::METHOD_NOT_ALLOWED, request_id)),
|
||||
}
|
||||
@@ -824,12 +988,8 @@ async fn route_request(
|
||||
handle_put_object(req, store, &bucket, &key, request_id).await
|
||||
}
|
||||
}
|
||||
Method::GET => {
|
||||
handle_get_object(req, store, &bucket, &key, request_id).await
|
||||
}
|
||||
Method::HEAD => {
|
||||
handle_head_object(store, &bucket, &key, request_id).await
|
||||
}
|
||||
Method::GET => handle_get_object(req, store, &bucket, &key, request_id).await,
|
||||
Method::HEAD => handle_head_object(store, &bucket, &key, request_id).await,
|
||||
Method::DELETE => {
|
||||
if query.contains_key("uploadId") {
|
||||
let upload_id = query.get("uploadId").unwrap();
|
||||
@@ -843,7 +1003,8 @@ async fn route_request(
|
||||
handle_initiate_multipart(req, store, &bucket, &key, request_id).await
|
||||
} else if query.contains_key("uploadId") {
|
||||
let upload_id = query.get("uploadId").unwrap().clone();
|
||||
handle_complete_multipart(req, store, &bucket, &key, &upload_id, request_id).await
|
||||
handle_complete_multipart(req, store, &bucket, &key, &upload_id, request_id)
|
||||
.await
|
||||
} else {
|
||||
let err = StorageError::invalid_request("Invalid POST request");
|
||||
Ok(storage_error_response(&err, request_id))
|
||||
@@ -972,7 +1133,13 @@ async fn handle_get_object(
|
||||
|
||||
let mut builder = Response::builder()
|
||||
.header("ETag", format!("\"{}\"", result.md5))
|
||||
.header("Last-Modified", result.last_modified.format("%a, %d %b %Y %H:%M:%S GMT").to_string())
|
||||
.header(
|
||||
"Last-Modified",
|
||||
result
|
||||
.last_modified
|
||||
.format("%a, %d %b %Y %H:%M:%S GMT")
|
||||
.to_string(),
|
||||
)
|
||||
.header("Content-Type", &content_type)
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.header("x-amz-request-id", request_id);
|
||||
@@ -1023,7 +1190,13 @@ async fn handle_head_object(
|
||||
let mut builder = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("ETag", format!("\"{}\"", result.md5))
|
||||
.header("Last-Modified", result.last_modified.format("%a, %d %b %Y %H:%M:%S GMT").to_string())
|
||||
.header(
|
||||
"Last-Modified",
|
||||
result
|
||||
.last_modified
|
||||
.format("%a, %d %b %Y %H:%M:%S GMT")
|
||||
.to_string(),
|
||||
)
|
||||
.header("Content-Type", &content_type)
|
||||
.header("Content-Length", result.size.to_string())
|
||||
.header("Accept-Ranges", "bytes")
|
||||
@@ -1086,7 +1259,14 @@ async fn handle_copy_object(
|
||||
};
|
||||
|
||||
let result = store
|
||||
.copy_object(&src_bucket, &src_key, dest_bucket, dest_key, &metadata_directive, new_metadata)
|
||||
.copy_object(
|
||||
&src_bucket,
|
||||
&src_key,
|
||||
dest_bucket,
|
||||
dest_key,
|
||||
&metadata_directive,
|
||||
new_metadata,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let xml = xml_response::copy_object_result_xml(&result.md5, &result.last_modified.to_rfc3339());
|
||||
@@ -1130,7 +1310,11 @@ async fn handle_put_bucket_policy(
|
||||
}
|
||||
|
||||
// Read body
|
||||
let body_bytes = req.collect().await.map_err(|e| anyhow::anyhow!("Body error: {}", e))?.to_bytes();
|
||||
let body_bytes = req
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Body error: {}", e))?
|
||||
.to_bytes();
|
||||
let body_str = String::from_utf8_lossy(&body_bytes);
|
||||
|
||||
// Validate and parse
|
||||
@@ -1212,7 +1396,11 @@ async fn handle_complete_multipart(
|
||||
request_id: &str,
|
||||
) -> Result<Response<BoxBody>> {
|
||||
// Read request body (XML)
|
||||
let body_bytes = req.collect().await.map_err(|e| anyhow::anyhow!("Body error: {}", e))?.to_bytes();
|
||||
let body_bytes = req
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Body error: {}", e))?
|
||||
.to_bytes();
|
||||
let body_str = String::from_utf8_lossy(&body_bytes);
|
||||
|
||||
// Parse parts from XML using regex-like approach
|
||||
@@ -1276,8 +1464,12 @@ fn extract_metadata(headers: &hyper::HeaderMap) -> HashMap<String, String> {
|
||||
let name_str = name.as_str().to_lowercase();
|
||||
if let Ok(val) = value.to_str() {
|
||||
match name_str.as_str() {
|
||||
"content-type" | "cache-control" | "content-disposition"
|
||||
| "content-encoding" | "content-language" | "expires" => {
|
||||
"content-type"
|
||||
| "cache-control"
|
||||
| "content-disposition"
|
||||
| "content-encoding"
|
||||
| "content-language"
|
||||
| "expires" => {
|
||||
metadata.insert(name_str, val.to_string());
|
||||
}
|
||||
_ if name_str.starts_with("x-amz-meta-") => {
|
||||
@@ -1290,7 +1482,10 @@ fn extract_metadata(headers: &hyper::HeaderMap) -> HashMap<String, String> {
|
||||
|
||||
// Default content-type
|
||||
if !metadata.contains_key("content-type") {
|
||||
metadata.insert("content-type".to_string(), "binary/octet-stream".to_string());
|
||||
metadata.insert(
|
||||
"content-type".to_string(),
|
||||
"binary/octet-stream".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
metadata
|
||||
@@ -1325,10 +1520,9 @@ fn parse_complete_multipart_xml(xml: &str) -> Vec<(u32, String)> {
|
||||
if let Some(part_end) = after_part.find("</Part>") {
|
||||
let part_content = &after_part[..part_end];
|
||||
|
||||
let part_number = extract_xml_value(part_content, "PartNumber")
|
||||
.and_then(|s| s.parse::<u32>().ok());
|
||||
let etag = extract_xml_value(part_content, "ETag")
|
||||
.map(|s| s.replace('"', ""));
|
||||
let part_number =
|
||||
extract_xml_value(part_content, "PartNumber").and_then(|s| s.parse::<u32>().ok());
|
||||
let etag = extract_xml_value(part_content, "ETag").map(|s| s.replace('"', ""));
|
||||
|
||||
if let (Some(pn), Some(et)) = (part_number, etag) {
|
||||
parts.push((pn, et));
|
||||
@@ -1394,9 +1588,6 @@ fn add_cors_headers(headers: &mut hyper::HeaderMap, config: &SmartStorageConfig)
|
||||
);
|
||||
}
|
||||
if config.cors.allow_credentials == Some(true) {
|
||||
headers.insert(
|
||||
"access-control-allow-credentials",
|
||||
"true".parse().unwrap(),
|
||||
);
|
||||
headers.insert("access-control-allow-credentials", "true".parse().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user