use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt::{self, Display};
use url::{ParseError, Url};
use crate::actions::{
AbortMultipartUpload, CreateBucket, DeleteBucket, DeleteObject, GetObject, HeadBucket,
HeadObject, PutObject, UploadPart,
};
#[cfg(feature = "full")]
use crate::actions::{
CompleteMultipartUpload, CreateMultipartUpload, DeleteObjects, ListObjectsV2, ListParts,
};
use crate::signing::util::percent_encode_path;
use crate::Credentials;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bucket {
base_url: Url,
name: Cow<'static, str>,
region: Cow<'static, str>,
}
#[derive(Debug, Clone, Copy)]
pub enum UrlStyle {
Path,
VirtualHost,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BucketError {
UnsupportedScheme,
MissingHost,
ParseError(ParseError),
}
impl From<ParseError> for BucketError {
fn from(error: ParseError) -> Self {
BucketError::ParseError(error)
}
}
impl Bucket {
pub fn new(
endpoint: Url,
path_style: UrlStyle,
name: impl Into<Cow<'static, str>>,
region: impl Into<Cow<'static, str>>,
) -> Result<Self, BucketError> {
endpoint.host_str().ok_or(BucketError::MissingHost)?;
match endpoint.scheme() {
"http" | "https" => {}
_ => return Err(BucketError::UnsupportedScheme),
};
let name = name.into();
let region = region.into();
let base_url = base_url(endpoint, &name, path_style)?;
Ok(Self {
base_url,
name,
region,
})
}
pub fn base_url(&self) -> &Url {
&self.base_url
}
pub fn name(&self) -> &str {
&self.name
}
pub fn region(&self) -> &str {
&self.region
}
pub fn object_url(&self, object: &str) -> Result<Url, ParseError> {
let object = percent_encode_path(object);
self.base_url.join(&object)
}
}
fn base_url(mut endpoint: Url, name: &str, path_style: UrlStyle) -> Result<Url, ParseError> {
match path_style {
UrlStyle::Path => {
let path = format!("{name}/");
endpoint.join(&path)
}
UrlStyle::VirtualHost => {
let host = format!("{}.{}", name, endpoint.host_str().unwrap());
endpoint.set_host(Some(&host))?;
Ok(endpoint)
}
}
}
impl Bucket {
pub fn create_bucket<'a>(&'a self, credentials: &'a Credentials) -> CreateBucket<'a> {
CreateBucket::new(self, credentials)
}
pub fn delete_bucket<'a>(&'a self, credentials: &'a Credentials) -> DeleteBucket<'a> {
DeleteBucket::new(self, credentials)
}
}
impl Bucket {
pub fn head_object<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
) -> HeadObject<'a> {
HeadObject::new(self, credentials, object)
}
pub fn head_bucket<'a>(&'a self, credentials: Option<&'a Credentials>) -> HeadBucket<'a> {
HeadBucket::new(self, credentials)
}
pub fn get_object<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
) -> GetObject<'a> {
GetObject::new(self, credentials, object)
}
#[cfg(feature = "full")]
pub fn list_objects_v2<'a>(
&'a self,
credentials: Option<&'a Credentials>,
) -> ListObjectsV2<'a> {
ListObjectsV2::new(self, credentials)
}
pub fn put_object<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
) -> PutObject<'a> {
PutObject::new(self, credentials, object)
}
pub fn delete_object<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
) -> DeleteObject<'a> {
DeleteObject::new(self, credentials, object)
}
#[cfg(feature = "full")]
pub fn delete_objects<'a, I>(
&'a self,
credentials: Option<&'a Credentials>,
objects: I,
) -> DeleteObjects<'a, I> {
DeleteObjects::new(self, credentials, objects)
}
}
impl Bucket {
#[cfg(feature = "full")]
pub fn create_multipart_upload<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
) -> CreateMultipartUpload<'a> {
CreateMultipartUpload::new(self, credentials, object)
}
pub fn upload_part<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
part_number: u16,
upload_id: &'a str,
) -> UploadPart<'a> {
UploadPart::new(self, credentials, object, part_number, upload_id)
}
#[cfg(feature = "full")]
pub fn complete_multipart_upload<'a, I>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
upload_id: &'a str,
etags: I,
) -> CompleteMultipartUpload<'a, I> {
CompleteMultipartUpload::new(self, credentials, object, upload_id, etags)
}
pub fn abort_multipart_upload<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
upload_id: &'a str,
) -> AbortMultipartUpload<'a> {
AbortMultipartUpload::new(self, credentials, object, upload_id)
}
#[cfg(feature = "full")]
pub fn list_parts<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
upload_id: &'a str,
) -> ListParts<'a> {
ListParts::new(self, credentials, object, upload_id)
}
}
impl Display for BucketError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedScheme => f.write_str("unsupported Url scheme"),
Self::MissingHost => f.write_str("Url is missing the `host`"),
Self::ParseError(e) => e.fmt(f),
}
}
}
impl StdError for BucketError {}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[cfg(feature = "full")]
use crate::actions::ObjectIdentifier;
#[test]
fn new_pathstyle() {
let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
.parse()
.unwrap();
let base_url: Url = "https://s3.dualstack.eu-west-1.amazonaws.com/rusty-s3/"
.parse()
.unwrap();
let name = "rusty-s3";
let region = "eu-west-1";
let bucket = Bucket::new(endpoint, UrlStyle::Path, name, region).unwrap();
assert_eq!(bucket.base_url(), &base_url);
assert_eq!(bucket.name(), name);
assert_eq!(bucket.region(), region);
}
#[test]
fn new_domainstyle() {
let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
.parse()
.unwrap();
let base_url: Url = "https://rusty-s3.s3.dualstack.eu-west-1.amazonaws.com"
.parse()
.unwrap();
let name = "rusty-s3";
let region = "eu-west-1";
let bucket = Bucket::new(endpoint, UrlStyle::VirtualHost, name, region).unwrap();
assert_eq!(bucket.base_url(), &base_url);
assert_eq!(bucket.name(), name);
assert_eq!(bucket.region(), region);
}
#[test]
fn new_bad_scheme() {
let endpoint = "ftp://example.com/example".parse().unwrap();
let name = "rusty-s3";
let region = "eu-west-1";
assert_eq!(
Bucket::new(endpoint, UrlStyle::Path, name, region),
Err(BucketError::UnsupportedScheme)
);
}
#[test]
fn new_missing_host() {
let endpoint = "file:///home/something".parse().unwrap();
let name = "rusty-s3";
let region = "eu-west-1";
assert_eq!(
Bucket::new(endpoint, UrlStyle::Path, name, region),
Err(BucketError::MissingHost)
);
}
#[test]
fn object_url_pathstyle() {
let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
.parse()
.unwrap();
let name = "rusty-s3";
let region = "eu-west-1";
let bucket = Bucket::new(endpoint, UrlStyle::Path, name, region).unwrap();
let path_style = bucket.object_url("something/cat.jpg").unwrap();
assert_eq!(
"https://s3.dualstack.eu-west-1.amazonaws.com/rusty-s3/something/cat.jpg",
path_style.as_str()
);
}
#[test]
fn object_url_domainstyle() {
let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
.parse()
.unwrap();
let name = "rusty-s3";
let region = "eu-west-1";
let bucket = Bucket::new(endpoint, UrlStyle::VirtualHost, name, region).unwrap();
let domain_style = bucket.object_url("something/cat.jpg").unwrap();
assert_eq!(
"https://rusty-s3.s3.dualstack.eu-west-1.amazonaws.com/something/cat.jpg",
domain_style.as_str()
);
}
#[test]
fn all_actions() {
let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
.parse()
.unwrap();
let name = "rusty-s3";
let region = "eu-west-1";
let bucket = Bucket::new(endpoint, UrlStyle::Path, name, region).unwrap();
let credentials = Credentials::new(
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
);
let _ = bucket.create_bucket(&credentials);
let _ = bucket.delete_bucket(&credentials);
let _ = bucket.head_object(Some(&credentials), "duck.jpg");
let _ = bucket.get_object(Some(&credentials), "duck.jpg");
#[cfg(feature = "full")]
let _ = bucket.list_objects_v2(Some(&credentials));
let _ = bucket.put_object(Some(&credentials), "duck.jpg");
let _ = bucket.delete_object(Some(&credentials), "duck.jpg");
#[cfg(feature = "full")]
let _ = bucket.delete_objects(Some(&credentials), std::iter::empty::<ObjectIdentifier>());
#[cfg(feature = "full")]
let _ = bucket.create_multipart_upload(Some(&credentials), "duck.jpg");
let _ = bucket.upload_part(Some(&credentials), "duck.jpg", 1, "abcd");
#[cfg(feature = "full")]
let _ = bucket.complete_multipart_upload(
Some(&credentials),
"duck.jpg",
"abcd",
["1234"].iter().copied(),
);
let _ = bucket.abort_multipart_upload(Some(&credentials), "duck.jpg", "abcd");
#[cfg(feature = "full")]
let _ = bucket.list_parts(Some(&credentials), "duck.jpg", "abcd");
}
}