BREAKING CHANGE(core): rebrand from smarts3 to smartstorage
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Failing after 26s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped

- Package renamed from @push.rocks/smarts3 to @push.rocks/smartstorage
- Class: Smarts3 → SmartStorage, Interface: ISmarts3Config → ISmartStorageConfig
- Method: getS3Descriptor → getStorageDescriptor
- Rust binary: rusts3 → ruststorage
- Rust types: S3Error→StorageError, S3Action→StorageAction, S3Config→SmartStorageConfig, S3Server→StorageServer
- On-disk file extension: ._S3_object → ._storage_object
- Default credentials: S3RVER → STORAGE
- All internal S3 branding removed; AWS S3 protocol compatibility fully maintained
This commit is contained in:
2026-03-14 15:20:30 +00:00
parent d437ffc226
commit bba0855218
26 changed files with 347 additions and 332 deletions

2
rust/Cargo.lock generated
View File

@@ -765,7 +765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "rusts3"
name = "ruststorage"
version = "0.1.0"
dependencies = [
"anyhow",

View File

@@ -1,10 +1,10 @@
[package]
name = "rusts3"
name = "ruststorage"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "rusts3"
name = "ruststorage"
path = "src/main.rs"
[dependencies]

View File

@@ -2,9 +2,9 @@ use hyper::body::Incoming;
use hyper::{Method, Request};
use std::collections::HashMap;
/// S3 actions that map to IAM permission strings.
/// Storage actions that map to IAM permission strings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum S3Action {
pub enum StorageAction {
ListAllMyBuckets,
CreateBucket,
DeleteBucket,
@@ -25,28 +25,28 @@ pub enum S3Action {
DeleteBucketPolicy,
}
impl S3Action {
impl StorageAction {
/// Return the IAM-style action string (e.g. "s3:GetObject").
pub fn iam_action(&self) -> &'static str {
match self {
S3Action::ListAllMyBuckets => "s3:ListAllMyBuckets",
S3Action::CreateBucket => "s3:CreateBucket",
S3Action::DeleteBucket => "s3:DeleteBucket",
S3Action::HeadBucket => "s3:ListBucket",
S3Action::ListBucket => "s3:ListBucket",
S3Action::GetObject => "s3:GetObject",
S3Action::HeadObject => "s3:GetObject",
S3Action::PutObject => "s3:PutObject",
S3Action::DeleteObject => "s3:DeleteObject",
S3Action::CopyObject => "s3:PutObject",
S3Action::ListBucketMultipartUploads => "s3:ListBucketMultipartUploads",
S3Action::AbortMultipartUpload => "s3:AbortMultipartUpload",
S3Action::InitiateMultipartUpload => "s3:PutObject",
S3Action::UploadPart => "s3:PutObject",
S3Action::CompleteMultipartUpload => "s3:PutObject",
S3Action::GetBucketPolicy => "s3:GetBucketPolicy",
S3Action::PutBucketPolicy => "s3:PutBucketPolicy",
S3Action::DeleteBucketPolicy => "s3:DeleteBucketPolicy",
StorageAction::ListAllMyBuckets => "s3:ListAllMyBuckets",
StorageAction::CreateBucket => "s3:CreateBucket",
StorageAction::DeleteBucket => "s3:DeleteBucket",
StorageAction::HeadBucket => "s3:ListBucket",
StorageAction::ListBucket => "s3:ListBucket",
StorageAction::GetObject => "s3:GetObject",
StorageAction::HeadObject => "s3:GetObject",
StorageAction::PutObject => "s3:PutObject",
StorageAction::DeleteObject => "s3:DeleteObject",
StorageAction::CopyObject => "s3:PutObject",
StorageAction::ListBucketMultipartUploads => "s3:ListBucketMultipartUploads",
StorageAction::AbortMultipartUpload => "s3:AbortMultipartUpload",
StorageAction::InitiateMultipartUpload => "s3:PutObject",
StorageAction::UploadPart => "s3:PutObject",
StorageAction::CompleteMultipartUpload => "s3:PutObject",
StorageAction::GetBucketPolicy => "s3:GetBucketPolicy",
StorageAction::PutBucketPolicy => "s3:PutBucketPolicy",
StorageAction::DeleteBucketPolicy => "s3:DeleteBucketPolicy",
}
}
}
@@ -54,7 +54,7 @@ impl S3Action {
/// Context extracted from a request, used for policy evaluation.
#[derive(Debug, Clone)]
pub struct RequestContext {
pub action: S3Action,
pub action: StorageAction,
pub bucket: Option<String>,
pub key: Option<String>,
}
@@ -70,7 +70,7 @@ impl RequestContext {
}
}
/// Resolve the S3 action from an incoming HTTP request.
/// Resolve the storage action from an incoming HTTP request.
pub fn resolve_action(req: &Request<Incoming>) -> RequestContext {
let method = req.method().clone();
let path = req.uri().path().to_string();
@@ -87,7 +87,7 @@ pub fn resolve_action(req: &Request<Incoming>) -> RequestContext {
0 => {
// Root: GET / -> ListBuckets
RequestContext {
action: S3Action::ListAllMyBuckets,
action: StorageAction::ListAllMyBuckets,
bucket: None,
key: None,
}
@@ -98,15 +98,15 @@ pub fn resolve_action(req: &Request<Incoming>) -> RequestContext {
let has_uploads = query.contains_key("uploads");
let action = match (&method, has_policy, has_uploads) {
(&Method::GET, true, _) => S3Action::GetBucketPolicy,
(&Method::PUT, true, _) => S3Action::PutBucketPolicy,
(&Method::DELETE, true, _) => S3Action::DeleteBucketPolicy,
(&Method::GET, _, true) => S3Action::ListBucketMultipartUploads,
(&Method::GET, _, _) => S3Action::ListBucket,
(&Method::PUT, _, _) => S3Action::CreateBucket,
(&Method::DELETE, _, _) => S3Action::DeleteBucket,
(&Method::HEAD, _, _) => S3Action::HeadBucket,
_ => S3Action::ListBucket,
(&Method::GET, true, _) => StorageAction::GetBucketPolicy,
(&Method::PUT, true, _) => StorageAction::PutBucketPolicy,
(&Method::DELETE, true, _) => StorageAction::DeleteBucketPolicy,
(&Method::GET, _, true) => StorageAction::ListBucketMultipartUploads,
(&Method::GET, _, _) => StorageAction::ListBucket,
(&Method::PUT, _, _) => StorageAction::CreateBucket,
(&Method::DELETE, _, _) => StorageAction::DeleteBucket,
(&Method::HEAD, _, _) => StorageAction::HeadBucket,
_ => StorageAction::ListBucket,
};
RequestContext {
@@ -125,16 +125,16 @@ pub fn resolve_action(req: &Request<Incoming>) -> RequestContext {
let has_uploads = query.contains_key("uploads");
let action = match &method {
&Method::PUT if has_part_number && has_upload_id => S3Action::UploadPart,
&Method::PUT if has_copy_source => S3Action::CopyObject,
&Method::PUT => S3Action::PutObject,
&Method::GET => S3Action::GetObject,
&Method::HEAD => S3Action::HeadObject,
&Method::DELETE if has_upload_id => S3Action::AbortMultipartUpload,
&Method::DELETE => S3Action::DeleteObject,
&Method::POST if has_uploads => S3Action::InitiateMultipartUpload,
&Method::POST if has_upload_id => S3Action::CompleteMultipartUpload,
_ => S3Action::GetObject,
&Method::PUT if has_part_number && has_upload_id => StorageAction::UploadPart,
&Method::PUT if has_copy_source => StorageAction::CopyObject,
&Method::PUT => StorageAction::PutObject,
&Method::GET => StorageAction::GetObject,
&Method::HEAD => StorageAction::HeadObject,
&Method::DELETE if has_upload_id => StorageAction::AbortMultipartUpload,
&Method::DELETE => StorageAction::DeleteObject,
&Method::POST if has_uploads => StorageAction::InitiateMultipartUpload,
&Method::POST if has_upload_id => StorageAction::CompleteMultipartUpload,
_ => StorageAction::GetObject,
};
RequestContext {
@@ -144,7 +144,7 @@ pub fn resolve_action(req: &Request<Incoming>) -> RequestContext {
}
}
_ => RequestContext {
action: S3Action::ListAllMyBuckets,
action: StorageAction::ListAllMyBuckets,
bucket: None,
key: None,
},

View File

@@ -4,8 +4,8 @@ use hyper::Request;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use crate::config::{Credential, S3Config};
use crate::s3_error::S3Error;
use crate::config::{Credential, SmartStorageConfig};
use crate::error::StorageError;
type HmacSha256 = Hmac<Sha256>;
@@ -27,8 +27,8 @@ struct SigV4Header {
/// Verify the request's SigV4 signature. Returns the caller identity on success.
pub fn verify_request(
req: &Request<Incoming>,
config: &S3Config,
) -> Result<AuthenticatedIdentity, S3Error> {
config: &SmartStorageConfig,
) -> Result<AuthenticatedIdentity, StorageError> {
let auth_header = req
.headers()
.get("authorization")
@@ -37,18 +37,18 @@ pub fn verify_request(
// Reject SigV2
if auth_header.starts_with("AWS ") {
return Err(S3Error::authorization_header_malformed());
return Err(StorageError::authorization_header_malformed());
}
if !auth_header.starts_with("AWS4-HMAC-SHA256") {
return Err(S3Error::authorization_header_malformed());
return Err(StorageError::authorization_header_malformed());
}
let parsed = parse_auth_header(auth_header)?;
// Look up credential
let credential = find_credential(&parsed.access_key_id, config)
.ok_or_else(S3Error::invalid_access_key_id)?;
.ok_or_else(StorageError::invalid_access_key_id)?;
// Get x-amz-date
let amz_date = req
@@ -60,7 +60,7 @@ pub fn verify_request(
.get("date")
.and_then(|v| v.to_str().ok())
})
.ok_or_else(|| S3Error::missing_security_header("Missing x-amz-date header"))?;
.ok_or_else(|| StorageError::missing_security_header("Missing x-amz-date header"))?;
// Enforce 15-min clock skew
check_clock_skew(amz_date)?;
@@ -99,7 +99,7 @@ pub fn verify_request(
// Constant-time comparison
if !constant_time_eq(computed_hex.as_bytes(), parsed.signature.as_bytes()) {
return Err(S3Error::signature_does_not_match());
return Err(StorageError::signature_does_not_match());
}
Ok(AuthenticatedIdentity {
@@ -108,11 +108,11 @@ pub fn verify_request(
}
/// Parse the Authorization header into its components.
fn parse_auth_header(header: &str) -> Result<SigV4Header, S3Error> {
fn parse_auth_header(header: &str) -> Result<SigV4Header, StorageError> {
// Format: AWS4-HMAC-SHA256 Credential=KEY/YYYYMMDD/region/s3/aws4_request, SignedHeaders=h1;h2, Signature=hex
let after_algo = header
.strip_prefix("AWS4-HMAC-SHA256")
.ok_or_else(S3Error::authorization_header_malformed)?
.ok_or_else(StorageError::authorization_header_malformed)?
.trim();
let mut credential_str = None;
@@ -131,17 +131,17 @@ fn parse_auth_header(header: &str) -> Result<SigV4Header, S3Error> {
}
let credential_str = credential_str
.ok_or_else(S3Error::authorization_header_malformed)?;
.ok_or_else(StorageError::authorization_header_malformed)?;
let signed_headers_str = signed_headers_str
.ok_or_else(S3Error::authorization_header_malformed)?;
.ok_or_else(StorageError::authorization_header_malformed)?;
let signature = signature_str
.ok_or_else(S3Error::authorization_header_malformed)?
.ok_or_else(StorageError::authorization_header_malformed)?
.to_string();
// Parse credential: KEY/YYYYMMDD/region/s3/aws4_request
let cred_parts: Vec<&str> = credential_str.splitn(5, '/').collect();
if cred_parts.len() < 5 {
return Err(S3Error::authorization_header_malformed());
return Err(StorageError::authorization_header_malformed());
}
let access_key_id = cred_parts[0].to_string();
@@ -163,7 +163,7 @@ fn parse_auth_header(header: &str) -> Result<SigV4Header, S3Error> {
}
/// Find a credential by access key ID.
fn find_credential<'a>(access_key_id: &str, config: &'a S3Config) -> Option<&'a Credential> {
fn find_credential<'a>(access_key_id: &str, config: &'a SmartStorageConfig) -> Option<&'a Credential> {
config
.auth
.credentials
@@ -172,17 +172,17 @@ fn find_credential<'a>(access_key_id: &str, config: &'a S3Config) -> Option<&'a
}
/// Check clock skew (15 minutes max).
fn check_clock_skew(amz_date: &str) -> Result<(), S3Error> {
fn check_clock_skew(amz_date: &str) -> Result<(), StorageError> {
// Parse ISO 8601 basic format: YYYYMMDDTHHMMSSZ
let parsed = chrono::NaiveDateTime::parse_from_str(amz_date, "%Y%m%dT%H%M%SZ")
.map_err(|_| S3Error::authorization_header_malformed())?;
.map_err(|_| StorageError::authorization_header_malformed())?;
let request_time = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(parsed, chrono::Utc);
let now = chrono::Utc::now();
let diff = (now - request_time).num_seconds().unsigned_abs();
if diff > 15 * 60 {
return Err(S3Error::request_time_too_skewed());
return Err(StorageError::request_time_too_skewed());
}
Ok(())

View File

@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct S3Config {
pub struct SmartStorageConfig {
pub server: ServerConfig,
pub storage: StorageConfig,
pub auth: AuthConfig,

View File

@@ -1,14 +1,14 @@
use hyper::StatusCode;
#[derive(Debug, thiserror::Error)]
#[error("S3Error({code}): {message}")]
pub struct S3Error {
#[error("StorageError({code}): {message}")]
pub struct StorageError {
pub code: String,
pub message: String,
pub status: StatusCode,
}
impl S3Error {
impl StorageError {
pub fn new(code: &str, message: &str, status: StatusCode) -> Self {
Self {
code: code.to_string(),

View File

@@ -3,7 +3,7 @@ mod auth;
mod config;
mod management;
mod policy;
mod s3_error;
mod error;
mod server;
mod storage;
mod xml_response;
@@ -11,7 +11,7 @@ mod xml_response;
use clap::Parser;
#[derive(Parser)]
#[command(name = "rusts3", about = "High-performance S3-compatible server")]
#[command(name = "ruststorage", about = "High-performance S3-compatible storage server")]
struct Cli {
/// Run in management mode (IPC via stdin/stdout)
#[arg(long)]
@@ -38,7 +38,7 @@ async fn main() -> anyhow::Result<()> {
management::management_loop().await?;
} else {
eprintln!("rusts3: use --management flag for IPC mode");
eprintln!("ruststorage: use --management flag for IPC mode");
std::process::exit(1);
}

View File

@@ -4,8 +4,8 @@ use serde_json::Value;
use std::io::Write;
use tokio::io::{AsyncBufReadExt, BufReader};
use crate::config::S3Config;
use crate::server::S3Server;
use crate::config::SmartStorageConfig;
use crate::server::StorageServer;
#[derive(Deserialize)]
struct IpcRequest {
@@ -62,7 +62,7 @@ pub async fn management_loop() -> Result<()> {
data: serde_json::json!({}),
});
let mut server: Option<S3Server> = None;
let mut server: Option<StorageServer> = None;
let stdin = BufReader::new(tokio::io::stdin());
let mut lines = stdin.lines();
@@ -87,11 +87,11 @@ pub async fn management_loop() -> Result<()> {
"start" => {
#[derive(Deserialize)]
struct StartParams {
config: S3Config,
config: SmartStorageConfig,
}
match serde_json::from_value::<StartParams>(req.params) {
Ok(params) => {
match S3Server::start(params.config).await {
match StorageServer::start(params.config).await {
Ok(s) => {
server = Some(s);
send_response(id, serde_json::json!({}));

View File

@@ -6,7 +6,7 @@ use tokio::sync::RwLock;
use crate::action::RequestContext;
use crate::auth::AuthenticatedIdentity;
use crate::s3_error::S3Error;
use crate::error::StorageError;
// ============================
// Policy data model
@@ -284,50 +284,50 @@ fn simple_wildcard_match(pattern: &str, value: &str) -> bool {
const MAX_POLICY_SIZE: usize = 20 * 1024; // 20 KB
pub fn validate_policy(json: &str) -> Result<BucketPolicy, S3Error> {
pub fn validate_policy(json: &str) -> Result<BucketPolicy, StorageError> {
if json.len() > MAX_POLICY_SIZE {
return Err(S3Error::malformed_policy("Policy exceeds maximum size of 20KB"));
return Err(StorageError::malformed_policy("Policy exceeds maximum size of 20KB"));
}
let policy: BucketPolicy =
serde_json::from_str(json).map_err(|e| S3Error::malformed_policy(&e.to_string()))?;
serde_json::from_str(json).map_err(|e| StorageError::malformed_policy(&e.to_string()))?;
if policy.version != "2012-10-17" {
return Err(S3Error::malformed_policy(
return Err(StorageError::malformed_policy(
"Policy version must be \"2012-10-17\"",
));
}
if policy.statements.is_empty() {
return Err(S3Error::malformed_policy(
return Err(StorageError::malformed_policy(
"Policy must contain at least one statement",
));
}
for (i, stmt) in policy.statements.iter().enumerate() {
if stmt.action.is_empty() {
return Err(S3Error::malformed_policy(&format!(
return Err(StorageError::malformed_policy(&format!(
"Statement {} has no actions",
i
)));
}
for action in &stmt.action {
if action != "*" && !action.starts_with("s3:") {
return Err(S3Error::malformed_policy(&format!(
return Err(StorageError::malformed_policy(&format!(
"Action \"{}\" must start with \"s3:\"",
action
)));
}
}
if stmt.resource.is_empty() {
return Err(S3Error::malformed_policy(&format!(
return Err(StorageError::malformed_policy(&format!(
"Statement {} has no resources",
i
)));
}
for resource in &stmt.resource {
if resource != "*" && !resource.starts_with("arn:aws:s3:::") {
return Err(S3Error::malformed_policy(&format!(
return Err(StorageError::malformed_policy(&format!(
"Resource \"{}\" must start with \"arn:aws:s3:::\"",
resource
)));

View File

@@ -18,22 +18,22 @@ use tokio::sync::watch;
use tokio_util::io::ReaderStream;
use uuid::Uuid;
use crate::action::{self, RequestContext, S3Action};
use crate::action::{self, RequestContext, StorageAction};
use crate::auth::{self, AuthenticatedIdentity};
use crate::config::S3Config;
use crate::config::SmartStorageConfig;
use crate::policy::{self, PolicyDecision, PolicyStore};
use crate::s3_error::S3Error;
use crate::error::StorageError;
use crate::storage::FileStore;
use crate::xml_response;
pub struct S3Server {
pub struct StorageServer {
store: Arc<FileStore>,
shutdown_tx: watch::Sender<bool>,
server_handle: tokio::task::JoinHandle<()>,
}
impl S3Server {
pub async fn start(config: S3Config) -> Result<Self> {
impl StorageServer {
pub async fn start(config: SmartStorageConfig) -> Result<Self> {
let store = Arc::new(FileStore::new(config.storage.directory.clone().into()));
// Initialize or reset storage
@@ -104,7 +104,7 @@ impl S3Server {
});
if !config.server.silent {
tracing::info!("S3 server listening on {}", addr);
tracing::info!("Storage server listening on {}", addr);
}
Ok(Self {
@@ -124,7 +124,7 @@ impl S3Server {
}
}
impl S3Config {
impl SmartStorageConfig {
fn address(&self) -> &str {
&self.server.address
}
@@ -192,7 +192,7 @@ fn empty_response(status: StatusCode, request_id: &str) -> Response<BoxBody> {
.unwrap()
}
fn s3_error_response(err: &S3Error, request_id: &str) -> Response<BoxBody> {
fn storage_error_response(err: &StorageError, request_id: &str) -> Response<BoxBody> {
let xml = err.to_xml();
Response::builder()
.status(err.status)
@@ -205,7 +205,7 @@ fn s3_error_response(err: &S3Error, request_id: &str) -> Response<BoxBody> {
async fn handle_request(
req: Request<Incoming>,
store: Arc<FileStore>,
config: S3Config,
config: SmartStorageConfig,
policy_store: Arc<PolicyStore>,
) -> Result<Response<BoxBody>, std::convert::Infallible> {
let request_id = Uuid::new_v4().to_string();
@@ -219,7 +219,7 @@ async fn handle_request(
return Ok(resp);
}
// Step 1: Resolve S3 action from request
// Step 1: Resolve storage action from request
let request_ctx = action::resolve_action(&req);
// Step 2: Auth + policy pipeline
@@ -238,7 +238,7 @@ async fn handle_request(
Ok(id) => Some(id),
Err(e) => {
tracing::warn!("Auth failed: {}", e.message);
return Ok(s3_error_response(&e, &request_id));
return Ok(storage_error_response(&e, &request_id));
}
}
} else {
@@ -248,7 +248,7 @@ async fn handle_request(
// Step 3: Authorization (policy evaluation)
if let Err(e) = authorize_request(&request_ctx, identity.as_ref(), &policy_store).await {
return Ok(s3_error_response(&e, &request_id));
return Ok(storage_error_response(&e, &request_id));
}
}
@@ -256,12 +256,12 @@ async fn handle_request(
let mut response = match route_request(req, store, &config, &request_id, &policy_store).await {
Ok(resp) => resp,
Err(err) => {
if let Some(s3err) = err.downcast_ref::<S3Error>() {
s3_error_response(s3err, &request_id)
if let Some(s3err) = err.downcast_ref::<StorageError>() {
storage_error_response(s3err, &request_id)
} else {
tracing::error!("Internal error: {}", err);
let s3err = S3Error::internal_error(&err.to_string());
s3_error_response(&s3err, &request_id)
let s3err = StorageError::internal_error(&err.to_string());
storage_error_response(&s3err, &request_id)
}
}
};
@@ -288,11 +288,11 @@ async fn authorize_request(
ctx: &RequestContext,
identity: Option<&AuthenticatedIdentity>,
policy_store: &PolicyStore,
) -> Result<(), S3Error> {
) -> Result<(), StorageError> {
// ListAllMyBuckets requires authentication (no bucket to apply policy to)
if ctx.action == S3Action::ListAllMyBuckets {
if ctx.action == StorageAction::ListAllMyBuckets {
if identity.is_none() {
return Err(S3Error::access_denied());
return Err(StorageError::access_denied());
}
return Ok(());
}
@@ -302,7 +302,7 @@ async fn authorize_request(
if let Some(bucket_policy) = policy_store.get_policy(bucket).await {
let decision = policy::evaluate_policy(&bucket_policy, ctx, identity);
match decision {
PolicyDecision::Deny => return Err(S3Error::access_denied()),
PolicyDecision::Deny => return Err(StorageError::access_denied()),
PolicyDecision::Allow => return Ok(()),
PolicyDecision::NoOpinion => {
// Fall through to default behavior
@@ -313,7 +313,7 @@ async fn authorize_request(
// Default: authenticated users get full access, anonymous denied
if identity.is_none() {
return Err(S3Error::access_denied());
return Err(StorageError::access_denied());
}
Ok(())
@@ -326,7 +326,7 @@ async fn authorize_request(
async fn route_request(
req: Request<Incoming>,
store: Arc<FileStore>,
_config: &S3Config,
_config: &SmartStorageConfig,
request_id: &str,
policy_store: &Arc<PolicyStore>,
) -> Result<Response<BoxBody>> {
@@ -414,8 +414,8 @@ async fn route_request(
let upload_id = query.get("uploadId").unwrap().clone();
handle_complete_multipart(req, store, &bucket, &key, &upload_id, request_id).await
} else {
let err = S3Error::invalid_request("Invalid POST request");
Ok(s3_error_response(&err, request_id))
let err = StorageError::invalid_request("Invalid POST request");
Ok(storage_error_response(&err, request_id))
}
}
_ => Ok(empty_response(StatusCode::METHOD_NOT_ALLOWED, request_id)),
@@ -467,7 +467,7 @@ async fn handle_head_bucket(
if store.bucket_exists(bucket).await {
Ok(empty_response(StatusCode::OK, request_id))
} else {
Err(S3Error::no_such_bucket().into())
Err(StorageError::no_such_bucket().into())
}
}
@@ -682,7 +682,7 @@ async fn handle_get_bucket_policy(
.unwrap();
Ok(resp)
}
None => Err(S3Error::no_such_bucket_policy().into()),
None => Err(StorageError::no_such_bucket_policy().into()),
}
}
@@ -695,7 +695,7 @@ async fn handle_put_bucket_policy(
) -> Result<Response<BoxBody>> {
// Verify bucket exists
if !store.bucket_exists(bucket).await {
return Err(S3Error::no_such_bucket().into());
return Err(StorageError::no_such_bucket().into());
}
// Read body
@@ -709,7 +709,7 @@ async fn handle_put_bucket_policy(
policy_store
.put_policy(bucket, validated_policy)
.await
.map_err(|e| S3Error::internal_error(&e.to_string()))?;
.map_err(|e| StorageError::internal_error(&e.to_string()))?;
Ok(empty_response(StatusCode::NO_CONTENT, request_id))
}
@@ -722,7 +722,7 @@ async fn handle_delete_bucket_policy(
policy_store
.delete_policy(bucket)
.await
.map_err(|e| S3Error::internal_error(&e.to_string()))?;
.map_err(|e| StorageError::internal_error(&e.to_string()))?;
Ok(empty_response(StatusCode::NO_CONTENT, request_id))
}
@@ -756,7 +756,7 @@ async fn handle_upload_part(
.unwrap_or(0);
if part_number < 1 || part_number > 10000 {
return Err(S3Error::invalid_part_number().into());
return Err(StorageError::invalid_part_number().into());
}
let body = req.into_body();
@@ -925,7 +925,7 @@ fn extract_xml_value<'a>(xml: &'a str, tag: &str) -> Option<String> {
// CORS
// ============================
fn build_cors_preflight(config: &S3Config, request_id: &str) -> Response<BoxBody> {
fn build_cors_preflight(config: &SmartStorageConfig, request_id: &str) -> Response<BoxBody> {
let mut builder = Response::builder()
.status(StatusCode::NO_CONTENT)
.header("x-amz-request-id", request_id);
@@ -949,7 +949,7 @@ fn build_cors_preflight(config: &S3Config, request_id: &str) -> Response<BoxBody
builder.body(empty_body()).unwrap()
}
fn add_cors_headers(headers: &mut hyper::HeaderMap, config: &S3Config) {
fn add_cors_headers(headers: &mut hyper::HeaderMap, config: &SmartStorageConfig) {
if let Some(ref origins) = config.cors.allowed_origins {
headers.insert(
"access-control-allow-origin",

View File

@@ -10,7 +10,7 @@ use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufWriter};
use uuid::Uuid;
use crate::s3_error::S3Error;
use crate::error::StorageError;
// ============================
// Result types
@@ -174,13 +174,13 @@ impl FileStore {
let bucket_path = self.root_dir.join(bucket);
if !bucket_path.is_dir() {
return Err(S3Error::no_such_bucket().into());
return Err(StorageError::no_such_bucket().into());
}
// Check if bucket is empty (ignore hidden files)
let mut entries = fs::read_dir(&bucket_path).await?;
while let Some(_entry) = entries.next_entry().await? {
return Err(S3Error::bucket_not_empty().into());
return Err(StorageError::bucket_not_empty().into());
}
fs::remove_dir_all(&bucket_path).await?;
@@ -199,7 +199,7 @@ impl FileStore {
metadata: HashMap<String, String>,
) -> Result<PutResult> {
if !self.bucket_exists(bucket).await {
return Err(S3Error::no_such_bucket().into());
return Err(StorageError::no_such_bucket().into());
}
let object_path = self.object_path(bucket, key);
@@ -256,7 +256,7 @@ impl FileStore {
let object_path = self.object_path(bucket, key);
if !object_path.exists() {
return Err(S3Error::no_such_key().into());
return Err(StorageError::no_such_key().into());
}
let file_meta = fs::metadata(&object_path).await?;
@@ -289,7 +289,7 @@ impl FileStore {
let object_path = self.object_path(bucket, key);
if !object_path.exists() {
return Err(S3Error::no_such_key().into());
return Err(StorageError::no_such_key().into());
}
// Only stat the file, don't open it
@@ -352,11 +352,11 @@ impl FileStore {
let dest_path = self.object_path(dest_bucket, dest_key);
if !src_path.exists() {
return Err(S3Error::no_such_key().into());
return Err(StorageError::no_such_key().into());
}
if !self.bucket_exists(dest_bucket).await {
return Err(S3Error::no_such_bucket().into());
return Err(StorageError::no_such_bucket().into());
}
if let Some(parent) = dest_path.parent() {
@@ -403,7 +403,7 @@ impl FileStore {
let bucket_path = self.root_dir.join(bucket);
if !bucket_path.is_dir() {
return Err(S3Error::no_such_bucket().into());
return Err(StorageError::no_such_bucket().into());
}
// Collect all object keys recursively
@@ -528,7 +528,7 @@ impl FileStore {
) -> Result<(String, u64)> {
let upload_dir = self.multipart_dir().join(upload_id);
if !upload_dir.is_dir() {
return Err(S3Error::no_such_upload().into());
return Err(StorageError::no_such_upload().into());
}
let part_path = upload_dir.join(format!("part-{}", part_number));
@@ -602,7 +602,7 @@ impl FileStore {
) -> Result<CompleteMultipartResult> {
let upload_dir = self.multipart_dir().join(upload_id);
if !upload_dir.is_dir() {
return Err(S3Error::no_such_upload().into());
return Err(StorageError::no_such_upload().into());
}
// Read metadata to get bucket/key
@@ -663,7 +663,7 @@ impl FileStore {
pub async fn abort_multipart(&self, upload_id: &str) -> Result<()> {
let upload_dir = self.multipart_dir().join(upload_id);
if !upload_dir.is_dir() {
return Err(S3Error::no_such_upload().into());
return Err(StorageError::no_such_upload().into());
}
fs::remove_dir_all(&upload_dir).await?;
Ok(())
@@ -715,7 +715,7 @@ impl FileStore {
let encoded = encode_key(key);
self.root_dir
.join(bucket)
.join(format!("{}._S3_object", encoded))
.join(format!("{}._storage_object", encoded))
}
async fn read_md5(&self, object_path: &Path) -> String {
@@ -775,7 +775,7 @@ impl FileStore {
if meta.is_dir() {
self.collect_keys(bucket_path, &entry.path(), keys).await?;
} else if name.ends_with("._S3_object")
} else if name.ends_with("._storage_object")
&& !name.ends_with(".metadata.json")
&& !name.ends_with(".md5")
{
@@ -785,7 +785,7 @@ impl FileStore {
.unwrap_or(Path::new(""))
.to_string_lossy()
.to_string();
let key = decode_key(relative.trim_end_matches("._S3_object"));
let key = decode_key(relative.trim_end_matches("._storage_object"));
keys.push(key);
}
}

View File

@@ -1,7 +1,7 @@
use crate::storage::{BucketInfo, ListObjectsResult, MultipartUploadInfo};
const XML_DECL: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
const S3_NS: &str = "http://s3.amazonaws.com/doc/2006-03-01/";
const STORAGE_NS: &str = "http://s3.amazonaws.com/doc/2006-03-01/";
fn xml_escape(s: &str) -> String {
s.replace('&', "&amp;")
@@ -14,9 +14,9 @@ fn xml_escape(s: &str) -> String {
pub fn list_buckets_xml(buckets: &[BucketInfo]) -> String {
let mut xml = format!(
"{}\n<ListAllMyBucketsResult xmlns=\"{}\">\
<Owner><ID>123456789000</ID><DisplayName>S3rver</DisplayName></Owner>\
<Owner><ID>123456789000</ID><DisplayName>Storage</DisplayName></Owner>\
<Buckets>",
XML_DECL, S3_NS
XML_DECL, STORAGE_NS
);
for b in buckets {
@@ -39,7 +39,7 @@ pub fn list_objects_v1_xml(bucket: &str, result: &ListObjectsResult) -> String {
<MaxKeys>{}</MaxKeys>\
<IsTruncated>{}</IsTruncated>",
XML_DECL,
S3_NS,
STORAGE_NS,
xml_escape(bucket),
xml_escape(&result.prefix),
result.max_keys,
@@ -86,7 +86,7 @@ pub fn list_objects_v2_xml(bucket: &str, result: &ListObjectsResult) -> String {
<KeyCount>{}</KeyCount>\
<IsTruncated>{}</IsTruncated>",
XML_DECL,
S3_NS,
STORAGE_NS,
xml_escape(bucket),
xml_escape(&result.prefix),
result.max_keys,
@@ -152,7 +152,7 @@ pub fn initiate_multipart_xml(bucket: &str, key: &str, upload_id: &str) -> Strin
<UploadId>{}</UploadId>\
</InitiateMultipartUploadResult>",
XML_DECL,
S3_NS,
STORAGE_NS,
xml_escape(bucket),
xml_escape(key),
xml_escape(upload_id)
@@ -168,7 +168,7 @@ pub fn complete_multipart_xml(bucket: &str, key: &str, etag: &str) -> String {
<ETag>\"{}\"</ETag>\
</CompleteMultipartUploadResult>",
XML_DECL,
S3_NS,
STORAGE_NS,
xml_escape(bucket),
xml_escape(key),
xml_escape(bucket),
@@ -186,7 +186,7 @@ pub fn list_multipart_uploads_xml(bucket: &str, uploads: &[MultipartUploadInfo])
<MaxUploads>1000</MaxUploads>\
<IsTruncated>false</IsTruncated>",
XML_DECL,
S3_NS,
STORAGE_NS,
xml_escape(bucket)
);
@@ -195,8 +195,8 @@ pub fn list_multipart_uploads_xml(bucket: &str, uploads: &[MultipartUploadInfo])
"<Upload>\
<Key>{}</Key>\
<UploadId>{}</UploadId>\
<Initiator><ID>S3RVER</ID><DisplayName>S3RVER</DisplayName></Initiator>\
<Owner><ID>S3RVER</ID><DisplayName>S3RVER</DisplayName></Owner>\
<Initiator><ID>STORAGE</ID><DisplayName>STORAGE</DisplayName></Initiator>\
<Owner><ID>STORAGE</ID><DisplayName>STORAGE</DisplayName></Owner>\
<StorageClass>STANDARD</StorageClass>\
<Initiated>{}</Initiated>\
</Upload>",