BREAKING CHANGE(core): rebrand from smarts3 to smartstorage
- 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:
2
rust/Cargo.lock
generated
2
rust/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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!({}));
|
||||
|
||||
@@ -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
|
||||
)));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('&', "&")
|
||||
@@ -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>",
|
||||
|
||||
Reference in New Issue
Block a user