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, GetBucketInfo,
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)]
36pub struct Bucket {
37 base_url: Url,
38 name: Cow<'static, str>,
39 region: Cow<'static, str>,
40}
41
42#[derive(Debug, Clone, Copy)]
44pub enum UrlStyle {
45 VirtualHost,
48}
49
50#[allow(clippy::module_name_repetitions)]
51#[derive(Debug, Copy, Clone, PartialEq, Eq)]
52pub enum BucketError {
53 UnsupportedScheme,
54 MissingHost,
55 ParseError(ParseError),
56}
57
58impl From<ParseError> for BucketError {
59 fn from(error: ParseError) -> Self {
60 Self::ParseError(error)
61 }
62}
63
64impl Bucket {
65 pub fn new(
71 endpoint: Url,
72 path_style: UrlStyle,
73 name: impl Into<Cow<'static, str>>,
74 region: impl Into<Cow<'static, str>>,
75 ) -> Result<Self, BucketError> {
76 endpoint.host_str().ok_or(BucketError::MissingHost)?;
77
78 match endpoint.scheme() {
79 "http" | "https" => {}
80 _ => return Err(BucketError::UnsupportedScheme),
81 };
82
83 let name = name.into();
84 let region = region.into();
85
86 let base_url = base_url(endpoint, &name, path_style)?;
87
88 Ok(Self {
89 base_url,
90 name,
91 region,
92 })
93 }
94
95 #[must_use]
97 pub const fn base_url(&self) -> &Url {
98 &self.base_url
99 }
100
101 #[must_use]
103 pub fn name(&self) -> &str {
104 &self.name
105 }
106
107 #[must_use]
109 pub fn region(&self) -> &str {
110 &self.region
111 }
112
113 pub fn object_url(&self, object: &str) -> Result<Url, ParseError> {
122 let object: Cow<'_, str> = percent_encode_path(object).into();
123 self.base_url.join(&object)
124 }
125}
126
127fn base_url(mut endpoint: Url, name: &str, path_style: UrlStyle) -> Result<Url, ParseError> {
128 match path_style {
129 UrlStyle::VirtualHost => {
130 let host = format!("{}.{}", name, endpoint.host_str().unwrap());
131 endpoint.set_host(Some(&host))?;
132 Ok(endpoint)
133 }
134 }
135}
136
137impl Bucket {
140 #[must_use]
144 pub fn create_bucket<'a>(&'a self, credentials: &'a Credentials) -> CreateBucket<'a> {
145 CreateBucket::new(self, credentials)
146 }
147
148 #[must_use]
152 pub fn delete_bucket<'a>(&'a self, credentials: &'a Credentials) -> DeleteBucket<'a> {
153 DeleteBucket::new(self, credentials)
154 }
155}
156
157impl Bucket {
160 #[must_use]
164 pub fn head_object<'a>(
165 &'a self,
166 credentials: Option<&'a Credentials>,
167 object: &'a str,
168 ) -> HeadObject<'a> {
169 HeadObject::new(self, credentials, object)
170 }
171
172 #[must_use]
176 pub fn get_bucket_info<'a>(&'a self, credentials: Option<&'a Credentials>) -> GetBucketInfo<'a> {
177 GetBucketInfo::new(self, credentials)
178 }
179
180 #[must_use]
184 pub fn get_object<'a>(
185 &'a self,
186 credentials: Option<&'a Credentials>,
187 object: &'a str,
188 ) -> GetObject<'a> {
189 GetObject::new(self, credentials, object)
190 }
191
192 #[cfg(feature = "full")]
196 #[must_use]
197 pub fn list_objects_v2<'a>(
198 &'a self,
199 credentials: Option<&'a Credentials>,
200 ) -> ListObjectsV2<'a> {
201 ListObjectsV2::new(self, credentials)
202 }
203
204 #[must_use]
208 pub fn put_object<'a>(
209 &'a self,
210 credentials: Option<&'a Credentials>,
211 object: &'a str,
212 ) -> PutObject<'a> {
213 PutObject::new(self, credentials, object)
214 }
215
216 #[must_use]
220 pub fn delete_object<'a>(
221 &'a self,
222 credentials: Option<&'a Credentials>,
223 object: &'a str,
224 ) -> DeleteObject<'a> {
225 DeleteObject::new(self, credentials, object)
226 }
227
228 #[cfg(feature = "full")]
232 pub fn delete_objects<'a, I>(
233 &'a self,
234 credentials: Option<&'a Credentials>,
235 objects: I,
236 ) -> DeleteObjects<'a, I> {
237 DeleteObjects::new(self, credentials, objects)
238 }
239}
240
241impl Bucket {
244 #[cfg(feature = "full")]
248 #[must_use]
249 pub fn create_multipart_upload<'a>(
250 &'a self,
251 credentials: Option<&'a Credentials>,
252 object: &'a str,
253 ) -> CreateMultipartUpload<'a> {
254 CreateMultipartUpload::new(self, credentials, object)
255 }
256
257 #[must_use]
261 pub fn upload_part<'a>(
262 &'a self,
263 credentials: Option<&'a Credentials>,
264 object: &'a str,
265 part_number: u16,
266 upload_id: &'a str,
267 ) -> UploadPart<'a> {
268 UploadPart::new(self, credentials, object, part_number, upload_id)
269 }
270
271 #[cfg(feature = "full")]
275 pub fn complete_multipart_upload<'a, I>(
276 &'a self,
277 credentials: Option<&'a Credentials>,
278 object: &'a str,
279 upload_id: &'a str,
280 etags: I,
281 ) -> CompleteMultipartUpload<'a, I> {
282 CompleteMultipartUpload::new(self, credentials, object, upload_id, etags)
283 }
284
285 #[must_use]
289 pub fn abort_multipart_upload<'a>(
290 &'a self,
291 credentials: Option<&'a Credentials>,
292 object: &'a str,
293 upload_id: &'a str,
294 ) -> AbortMultipartUpload<'a> {
295 AbortMultipartUpload::new(self, credentials, object, upload_id)
296 }
297
298 #[cfg(feature = "full")]
302 #[must_use]
303 pub fn list_parts<'a>(
304 &'a self,
305 credentials: Option<&'a Credentials>,
306 object: &'a str,
307 upload_id: &'a str,
308 ) -> ListParts<'a> {
309 ListParts::new(self, credentials, object, upload_id)
310 }
311}
312
313impl Display for BucketError {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 match *self {
316 Self::UnsupportedScheme => f.write_str("unsupported Url scheme"),
317 Self::MissingHost => f.write_str("Url is missing the `host`"),
318 Self::ParseError(e) => e.fmt(f),
319 }
320 }
321}
322
323impl StdError for BucketError {}
324
325#[cfg(test)]
326mod tests {
327 use pretty_assertions::assert_eq;
328
329 use super::*;
330 #[cfg(feature = "full")]
331 use crate::actions::ObjectIdentifier;
332
333 #[test]
334 fn new_domainstyle() {
335 let endpoint: Url = "https://oss-cn-hangzhou.aliyuncs.com"
336 .parse()
337 .unwrap();
338 let base_url: Url = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com"
339 .parse()
340 .unwrap();
341 let name = "examplebucket";
342 let region = "cn-hangzhou";
343 let bucket = Bucket::new(endpoint, UrlStyle::VirtualHost, name, region).unwrap();
344
345 assert_eq!(bucket.base_url(), &base_url);
346 assert_eq!(bucket.name(), name);
347 assert_eq!(bucket.region(), region);
348 }
349
350 #[test]
351 fn new_bad_scheme() {
352 let endpoint = "ftp://oss-cn-hangzhou.aliyuncs.com".parse().unwrap();
353 let name = "examplebucket";
354 let region = "cn-hangzhou";
355 assert_eq!(
356 Bucket::new(endpoint, UrlStyle::VirtualHost, name, region),
357 Err(BucketError::UnsupportedScheme)
358 );
359 }
360
361 #[test]
362 fn new_missing_host() {
363 let endpoint = "file:///home/something".parse().unwrap();
364 let name = "examplebucket";
365 let region = "cn-hangzhou";
366 assert_eq!(
367 Bucket::new(endpoint, UrlStyle::VirtualHost, name, region),
368 Err(BucketError::MissingHost)
369 );
370 }
371
372 #[test]
373 fn object_url_domainstyle() {
374 let endpoint: Url = "https://oss-cn-hangzhou.aliyuncs.com"
375 .parse()
376 .unwrap();
377 let name = "examplebucket";
378 let region = "cn-hangzhou";
379 let bucket = Bucket::new(endpoint, UrlStyle::VirtualHost, name, region).unwrap();
380
381 let domain_style = bucket.object_url("something/cat.jpg").unwrap();
382 assert_eq!(
383 "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/something/cat.jpg",
384 domain_style.as_str()
385 );
386 }
387
388 #[test]
389 fn all_actions() {
390 let endpoint: Url = "https://oss-cn-hangzhou.aliyuncs.com"
391 .parse()
392 .unwrap();
393
394 let name = "examplebucket";
395 let region = "cn-hangzhou";
396 let bucket = Bucket::new(endpoint, UrlStyle::VirtualHost, name, region).unwrap();
397
398 let credentials = Credentials::new(
399 "access_key_id",
400 "access_key_secret",
401 );
402
403 let _ = bucket.create_bucket(&credentials);
404 let _ = bucket.delete_bucket(&credentials);
405
406 let _ = bucket.head_object(Some(&credentials), "duck.jpg");
407 let _ = bucket.get_object(Some(&credentials), "duck.jpg");
408 #[cfg(feature = "full")]
409 let _ = bucket.list_objects_v2(Some(&credentials));
410 let _ = bucket.put_object(Some(&credentials), "duck.jpg");
411 let _ = bucket.delete_object(Some(&credentials), "duck.jpg");
412 #[cfg(feature = "full")]
413 let _ = bucket.delete_objects(Some(&credentials), std::iter::empty::<ObjectIdentifier>());
414
415 #[cfg(feature = "full")]
416 let _ = bucket.create_multipart_upload(Some(&credentials), "duck.jpg");
417 let _ = bucket.upload_part(Some(&credentials), "duck.jpg", 1, "abcd");
418 #[cfg(feature = "full")]
419 let _ = bucket.complete_multipart_upload(
420 Some(&credentials),
421 "duck.jpg",
422 "abcd",
423 ["1234"].iter().copied(),
424 );
425 let _ = bucket.abort_multipart_upload(Some(&credentials), "duck.jpg", "abcd");
426 #[cfg(feature = "full")]
427 let _ = bucket.list_parts(Some(&credentials), "duck.jpg", "abcd");
428 }
429}