pub mod actions;
pub mod checksum;
pub mod credentials;
pub mod error;
pub mod limits;
pub mod object_lock;
pub mod options;
pub mod region;
pub mod request;
pub mod responses;
pub mod signature;
pub mod tools;
pub use self::{
credentials::Credentials,
error::{ApiError, Error},
object_lock::{ObjectLock, ObjectLockMode},
options::RequestOptions,
region::Region,
signature::Signature,
};
use crate::s3::error::Result;
use crate::s3::tools::{sha256_digest, write_hex_bytes};
use reqwest::Client;
use std::fmt;
use url::Url;
#[derive(Debug, Clone)]
pub struct S3 {
credentials: Credentials,
region: Region,
bucket: Option<String>,
no_sign_request: bool,
client: Client,
}
impl fmt::Display for S3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"credentials: {:#?}\nregion: {:#?}\nbucket: {:#?}\nno_sign_request: {}",
self.credentials, self.region, self.bucket, self.no_sign_request
)
}
}
impl S3 {
#[must_use]
pub fn new(
credentials: &Credentials,
region: &Region,
bucket: Option<String>,
no_sign_request: bool,
) -> Self {
Self {
credentials: credentials.clone(),
region: region.clone(),
bucket,
no_sign_request,
client: Client::new(),
}
}
#[must_use]
pub const fn client(&self) -> &Client {
&self.client
}
#[must_use]
pub fn bucket(&self) -> Option<&str> {
self.bucket.as_deref()
}
#[must_use]
pub fn with_bucket(&self, bucket: Option<String>) -> Self {
let mut s3 = self.clone();
s3.bucket = bucket;
s3
}
#[must_use]
pub fn hash(&self) -> String {
let mut hash = String::new();
hash.push_str(self.credentials.aws_access_key_id());
hash.push_str(self.credentials.aws_secret_access_key());
hash.push_str(&self.region.endpoint());
if let Some(bucket) = &self.bucket {
hash.push_str(bucket);
}
write_hex_bytes(sha256_digest(&hash).as_ref())
}
pub fn endpoint(&self) -> Result<Url> {
let endpoint_str = self.region.endpoint();
let base_url =
if endpoint_str.starts_with("http://") || endpoint_str.starts_with("https://") {
endpoint_str
} else {
format!("https://{endpoint_str}")
};
let url = if let Some(bucket) = &self.bucket {
Url::parse(&format!("{base_url}/{bucket}"))?
} else {
Url::parse(&base_url)?
};
Ok(url)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::unnecessary_wraps
)]
mod tests {
use super::*;
use secrecy::SecretString;
#[test]
fn test_s3() {
let s3 = S3::new(
&Credentials::new(
"AKIAIOSFODNN7EXAMPLE",
&SecretString::new("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into()),
),
&"us-west-1".parse::<Region>().unwrap(),
Some("awsexamplebucket1".to_string()),
false,
);
assert_eq!(
s3.hash(),
"bc8cea3065fd34a0b9cda467e3ace6bae7737d046e0dd3d449381a33508c24ae"
);
}
#[test]
fn test_s3_endpoint() {
let s3 = S3::new(
&Credentials::new(
"AKIAIOSFODNN7EXAMPLE",
&SecretString::new("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into()),
),
&"us-west-1".parse::<Region>().unwrap(),
Some("awsexamplebucket1".to_string()),
false,
);
assert_eq!(
s3.endpoint().unwrap().as_str(),
"https://s3.us-west-1.amazonaws.com/awsexamplebucket1"
);
}
}