1use std::borrow::Cow;
2use std::error::Error as StdError;
3use std::fmt::{self, Display};
4
5use url::{ParseError, Url};
6
7use crate::actions::{
8 AbortMultipartUpload, CreateBucket, DeleteBucket, DeleteObject, GetObject, HeadBucket,
9 HeadObject, PutObject, UploadPart,
10};
11#[cfg(feature = "full")]
12use crate::actions::{
13 CompleteMultipartUpload, CreateMultipartUpload, DeleteObjects, ListObjectsV2, ListParts,
14};
15use crate::signing::util::percent_encode_path;
16use crate::Credentials;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Bucket {
53 base_url: Url,
54 name: Cow<'static, str>,
55 region: Cow<'static, str>,
56}
57
58#[derive(Debug, Clone, Copy)]
60pub enum UrlStyle {
61 Path,
68 VirtualHost,
71}
72
73#[allow(clippy::module_name_repetitions)]
74#[derive(Debug, Copy, Clone, PartialEq, Eq)]
75pub enum BucketError {
76 UnsupportedScheme,
77 MissingHost,
78 ParseError(ParseError),
79}
80
81impl From<ParseError> for BucketError {
82 fn from(error: ParseError) -> Self {
83 Self::ParseError(error)
84 }
85}
86
87impl Bucket {
88 pub fn new(
94 endpoint: Url,
95 path_style: UrlStyle,
96 name: impl Into<Cow<'static, str>>,
97 region: impl Into<Cow<'static, str>>,
98 ) -> Result<Self, BucketError> {
99 endpoint.host_str().ok_or(BucketError::MissingHost)?;
100
101 match endpoint.scheme() {
102 "http" | "https" => {}
103 _ => return Err(BucketError::UnsupportedScheme),
104 };
105
106 let name = name.into();
107 let region = region.into();
108
109 let base_url = base_url(endpoint, &name, path_style)?;
110
111 Ok(Self {
112 base_url,
113 name,
114 region,
115 })
116 }
117
118 #[must_use]
120 pub const fn base_url(&self) -> &Url {
121 &self.base_url
122 }
123
124 #[must_use]
126 pub fn name(&self) -> &str {
127 &self.name
128 }
129
130 #[must_use]
132 pub fn region(&self) -> &str {
133 &self.region
134 }
135
136 pub fn object_url(&self, object: &str) -> Result<Url, ParseError> {
145 let object: Cow<'_, str> = percent_encode_path(object).into();
146 self.base_url.join(&object)
147 }
148}
149
150fn base_url(mut endpoint: Url, name: &str, path_style: UrlStyle) -> Result<Url, ParseError> {
151 match path_style {
152 UrlStyle::Path => {
153 let path = format!("{name}/");
154 endpoint.join(&path)
155 }
156 UrlStyle::VirtualHost => {
157 let host = format!("{}.{}", name, endpoint.host_str().unwrap());
158 endpoint.set_host(Some(&host))?;
159 Ok(endpoint)
160 }
161 }
162}
163
164impl Bucket {
167 #[must_use]
171 pub const fn create_bucket<'a>(&'a self, credentials: &'a Credentials) -> CreateBucket<'a> {
172 CreateBucket::new(self, credentials)
173 }
174
175 #[must_use]
179 pub const fn delete_bucket<'a>(&'a self, credentials: &'a Credentials) -> DeleteBucket<'a> {
180 DeleteBucket::new(self, credentials)
181 }
182}
183
184impl Bucket {
187 #[must_use]
191 pub const fn head_object<'a>(
192 &'a self,
193 credentials: Option<&'a Credentials>,
194 object: &'a str,
195 ) -> HeadObject<'a> {
196 HeadObject::new(self, credentials, object)
197 }
198
199 #[must_use]
203 pub const fn head_bucket<'a>(&'a self, credentials: Option<&'a Credentials>) -> HeadBucket<'a> {
204 HeadBucket::new(self, credentials)
205 }
206
207 #[must_use]
211 pub const fn get_object<'a>(
212 &'a self,
213 credentials: Option<&'a Credentials>,
214 object: &'a str,
215 ) -> GetObject<'a> {
216 GetObject::new(self, credentials, object)
217 }
218
219 #[cfg(feature = "full")]
223 #[must_use]
224 pub fn list_objects_v2<'a>(
225 &'a self,
226 credentials: Option<&'a Credentials>,
227 ) -> ListObjectsV2<'a> {
228 ListObjectsV2::new(self, credentials)
229 }
230
231 #[must_use]
235 pub const fn put_object<'a>(
236 &'a self,
237 credentials: Option<&'a Credentials>,
238 object: &'a str,
239 ) -> PutObject<'a> {
240 PutObject::new(self, credentials, object)
241 }
242
243 #[must_use]
247 pub const fn delete_object<'a>(
248 &'a self,
249 credentials: Option<&'a Credentials>,
250 object: &'a str,
251 ) -> DeleteObject<'a> {
252 DeleteObject::new(self, credentials, object)
253 }
254
255 #[cfg(feature = "full")]
259 pub const fn delete_objects<'a, I>(
260 &'a self,
261 credentials: Option<&'a Credentials>,
262 objects: I,
263 ) -> DeleteObjects<'a, I> {
264 DeleteObjects::new(self, credentials, objects)
265 }
266}
267
268impl Bucket {
271 #[cfg(feature = "full")]
275 #[must_use]
276 pub const fn create_multipart_upload<'a>(
277 &'a self,
278 credentials: Option<&'a Credentials>,
279 object: &'a str,
280 ) -> CreateMultipartUpload<'a> {
281 CreateMultipartUpload::new(self, credentials, object)
282 }
283
284 #[must_use]
288 pub const fn upload_part<'a>(
289 &'a self,
290 credentials: Option<&'a Credentials>,
291 object: &'a str,
292 part_number: u16,
293 upload_id: &'a str,
294 ) -> UploadPart<'a> {
295 UploadPart::new(self, credentials, object, part_number, upload_id)
296 }
297
298 #[cfg(feature = "full")]
302 pub const fn complete_multipart_upload<'a, I>(
303 &'a self,
304 credentials: Option<&'a Credentials>,
305 object: &'a str,
306 upload_id: &'a str,
307 etags: I,
308 ) -> CompleteMultipartUpload<'a, I> {
309 CompleteMultipartUpload::new(self, credentials, object, upload_id, etags)
310 }
311
312 #[must_use]
316 pub const fn abort_multipart_upload<'a>(
317 &'a self,
318 credentials: Option<&'a Credentials>,
319 object: &'a str,
320 upload_id: &'a str,
321 ) -> AbortMultipartUpload<'a> {
322 AbortMultipartUpload::new(self, credentials, object, upload_id)
323 }
324
325 #[cfg(feature = "full")]
329 #[must_use]
330 pub const fn list_parts<'a>(
331 &'a self,
332 credentials: Option<&'a Credentials>,
333 object: &'a str,
334 upload_id: &'a str,
335 ) -> ListParts<'a> {
336 ListParts::new(self, credentials, object, upload_id)
337 }
338}
339
340impl Display for BucketError {
341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342 match *self {
343 Self::UnsupportedScheme => f.write_str("unsupported Url scheme"),
344 Self::MissingHost => f.write_str("Url is missing the `host`"),
345 Self::ParseError(e) => e.fmt(f),
346 }
347 }
348}
349
350impl StdError for BucketError {}
351
352#[cfg(test)]
353mod tests {
354 use pretty_assertions::assert_eq;
355
356 use super::*;
357 #[cfg(feature = "full")]
358 use crate::actions::ObjectIdentifier;
359
360 #[test]
361 fn new_pathstyle() {
362 let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
363 .parse()
364 .unwrap();
365 let base_url: Url = "https://s3.dualstack.eu-west-1.amazonaws.com/rusty-s3/"
366 .parse()
367 .unwrap();
368 let name = "rusty-s3";
369 let region = "eu-west-1";
370 let bucket = Bucket::new(endpoint, UrlStyle::Path, name, region).unwrap();
371
372 assert_eq!(bucket.base_url(), &base_url);
373 assert_eq!(bucket.name(), name);
374 assert_eq!(bucket.region(), region);
375 }
376
377 #[test]
378 fn new_domainstyle() {
379 let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
380 .parse()
381 .unwrap();
382 let base_url: Url = "https://rusty-s3.s3.dualstack.eu-west-1.amazonaws.com"
383 .parse()
384 .unwrap();
385 let name = "rusty-s3";
386 let region = "eu-west-1";
387 let bucket = Bucket::new(endpoint, UrlStyle::VirtualHost, name, region).unwrap();
388
389 assert_eq!(bucket.base_url(), &base_url);
390 assert_eq!(bucket.name(), name);
391 assert_eq!(bucket.region(), region);
392 }
393
394 #[test]
395 fn new_bad_scheme() {
396 let endpoint = "ftp://example.com/example".parse().unwrap();
397 let name = "rusty-s3";
398 let region = "eu-west-1";
399 assert_eq!(
400 Bucket::new(endpoint, UrlStyle::Path, name, region),
401 Err(BucketError::UnsupportedScheme)
402 );
403 }
404
405 #[test]
406 fn new_missing_host() {
407 let endpoint = "file:///home/something".parse().unwrap();
408 let name = "rusty-s3";
409 let region = "eu-west-1";
410 assert_eq!(
411 Bucket::new(endpoint, UrlStyle::Path, name, region),
412 Err(BucketError::MissingHost)
413 );
414 }
415
416 #[test]
417 fn object_url_pathstyle() {
418 let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
419 .parse()
420 .unwrap();
421 let name = "rusty-s3";
422 let region = "eu-west-1";
423 let bucket = Bucket::new(endpoint, UrlStyle::Path, name, region).unwrap();
424
425 let path_style = bucket.object_url("something/cat.jpg").unwrap();
426 assert_eq!(
427 "https://s3.dualstack.eu-west-1.amazonaws.com/rusty-s3/something/cat.jpg",
428 path_style.as_str()
429 );
430 }
431
432 #[test]
433 fn object_url_domainstyle() {
434 let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
435 .parse()
436 .unwrap();
437 let name = "rusty-s3";
438 let region = "eu-west-1";
439 let bucket = Bucket::new(endpoint, UrlStyle::VirtualHost, name, region).unwrap();
440
441 let domain_style = bucket.object_url("something/cat.jpg").unwrap();
442 assert_eq!(
443 "https://rusty-s3.s3.dualstack.eu-west-1.amazonaws.com/something/cat.jpg",
444 domain_style.as_str()
445 );
446 }
447
448 #[test]
449 fn all_actions() {
450 let endpoint: Url = "https://s3.dualstack.eu-west-1.amazonaws.com"
451 .parse()
452 .unwrap();
453
454 let name = "rusty-s3";
455 let region = "eu-west-1";
456 let bucket = Bucket::new(endpoint, UrlStyle::Path, name, region).unwrap();
457
458 let credentials = Credentials::new(
459 "AKIAIOSFODNN7EXAMPLE",
460 "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
461 );
462
463 let _ = bucket.create_bucket(&credentials);
464 let _ = bucket.delete_bucket(&credentials);
465
466 let _ = bucket.head_object(Some(&credentials), "duck.jpg");
467 let _ = bucket.get_object(Some(&credentials), "duck.jpg");
468 #[cfg(feature = "full")]
469 let _ = bucket.list_objects_v2(Some(&credentials));
470 let _ = bucket.put_object(Some(&credentials), "duck.jpg");
471 let _ = bucket.delete_object(Some(&credentials), "duck.jpg");
472 #[cfg(feature = "full")]
473 let _ = bucket.delete_objects(Some(&credentials), std::iter::empty::<ObjectIdentifier>());
474
475 #[cfg(feature = "full")]
476 let _ = bucket.create_multipart_upload(Some(&credentials), "duck.jpg");
477 let _ = bucket.upload_part(Some(&credentials), "duck.jpg", 1, "abcd");
478 #[cfg(feature = "full")]
479 let _ = bucket.complete_multipart_upload(
480 Some(&credentials),
481 "duck.jpg",
482 "abcd",
483 ["1234"].iter().copied(),
484 );
485 let _ = bucket.abort_multipart_upload(Some(&credentials), "duck.jpg", "abcd");
486 #[cfg(feature = "full")]
487 let _ = bucket.list_parts(Some(&credentials), "duck.jpg", "abcd");
488 }
489}