1use crate::command::{Command, CompleteMultipartUploadData, Part};
2use crate::constants::LONG_DATE_TIME;
3use crate::credentials::Credentials;
4use crate::error::S3Error;
5use crate::types::Multipart;
6use crate::types::{
7 HeadObjectResult, InitiateMultipartUploadResponse, ListBucketResult, PutStreamResponse,
8};
9use crate::{Region, S3Response, S3StatusCode, md5_url_encode, signature};
10use hmac::{Hmac, KeyInit};
11use http::header::{ACCEPT, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, DATE, HOST, RANGE};
12use http::{HeaderMap, HeaderName, HeaderValue};
13use reqwest::Response;
14use sha2::Sha256;
15use sha2::digest::Mac;
16use std::fmt::Write;
17use std::sync::OnceLock;
18use std::time::Duration;
19use std::{env, mem};
20use time::OffsetDateTime;
21use time::format_description::well_known::Rfc2822;
22use tokio::io::{AsyncRead, AsyncReadExt};
23use tracing::{debug, error, warn};
24use url::Url;
25
26static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
27
28const CHUNK_SIZE: usize = 8 * 1024 * 1024; enum StreamChunk {
32 Data(Vec<u8>),
33 End,
34 Error(std::io::Error),
35}
36
37#[derive(Debug)]
38pub struct BucketOptions {
39 pub path_style: bool,
40 pub list_objects_v2: bool,
41}
42
43impl Default for BucketOptions {
44 fn default() -> Self {
45 Self {
46 path_style: env::var("S3_PATH_STYLE").as_deref() == Ok("true"),
47 list_objects_v2: true,
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
53pub struct Bucket {
54 pub host: Url,
55 pub name: String,
56 pub region: Region,
57 pub credentials: Credentials,
58 path_style: bool,
59 list_objects_v2: bool,
60}
61
62#[allow(dead_code)]
63#[allow(clippy::assigning_clones)] impl Bucket {
65 fn host_domain(&self) -> String {
66 match self.host.domain() {
67 None => {
68 let host_str = self
70 .host
71 .host_str()
72 .expect("host_str to exist when domain does not");
73 if let Some(port) = self.host.port() {
74 format!("{}:{}", host_str, port,)
75 } else {
76 host_str.to_string()
77 }
78 }
79 Some(domain) => {
80 if let Some(port) = self.host.port() {
81 format!("{}:{}", domain, port)
82 } else {
83 domain.to_string()
84 }
85 }
86 }
87 }
88
89 pub fn new(
90 host: Url,
91 name: String,
92 region: Region,
93 credentials: Credentials,
94 options: Option<BucketOptions>,
95 ) -> Result<Self, S3Error> {
96 let options = options.unwrap_or_default();
97 Ok(Self {
98 host,
99 name,
100 region,
101 credentials,
102 path_style: options.path_style,
103 list_objects_v2: options.list_objects_v2,
104 })
105 }
106
107 pub fn try_from_env() -> Result<Self, S3Error> {
108 let host_env = env::var("S3_URL")?;
109 let host = host_env.parse::<Url>()?;
110
111 let name = env::var("S3_BUCKET")?;
112 let region = Region::try_from_env()?;
113 let credentials = Credentials::try_from_env()?;
114 let options = BucketOptions::default();
115
116 Ok(Self {
117 host,
118 name,
119 region,
120 credentials,
121 path_style: options.path_style,
122 list_objects_v2: options.list_objects_v2,
123 })
124 }
125
126 pub async fn head<S: AsRef<str>>(&self, path: S) -> Result<HeadObjectResult, S3Error> {
128 let res = self
129 .send_request(Command::HeadObject, path.as_ref())
130 .await?;
131 Ok(HeadObjectResult::from(res.headers()))
132 }
133
134 pub async fn get<P>(&self, path: P) -> Result<S3Response, S3Error>
136 where
137 P: AsRef<str>,
138 {
139 self.send_request(Command::GetObject, path.as_ref()).await
140 }
141
142 pub async fn get_range<S: AsRef<str>>(
143 &self,
144 path: S,
145 start: u64,
146 end: Option<u64>,
147 ) -> Result<S3Response, S3Error> {
148 if let Some(end) = end && start > end {
151 return Err(S3Error::Range("start must be <= than end"));
152 }
153 self.send_request(Command::GetObjectRange { start, end }, path.as_ref())
154 .await
155 }
156
157 pub async fn delete<S: AsRef<str>>(&self, path: S) -> Result<S3Response, S3Error> {
159 self.send_request(Command::DeleteObject, path.as_ref())
160 .await
161 }
162
163 pub async fn put<S: AsRef<str>>(&self, path: S, content: &[u8]) -> Result<S3Response, S3Error> {
165 self.put_with_content_type(path, content, "application/octet-stream")
166 .await
167 }
168
169 pub async fn put_with_content_type<S: AsRef<str>>(
171 &self,
172 path: S,
173 content: &[u8],
174 content_type: &str,
175 ) -> Result<S3Response, S3Error> {
176 let mut headers = HeaderMap::new();
177 headers.insert(CONTENT_TYPE, HeaderValue::from_str(content_type)?);
178
179 self.send_request(
180 Command::PutObject {
181 content,
182 headers,
183 multipart: None,
184 },
185 path.as_ref(),
186 )
187 .await
188 }
189
190 pub async fn put_with<S: AsRef<str>>(
211 &self,
212 path: S,
213 content: &[u8],
214 extra_headers: HeaderMap,
215 ) -> Result<S3Response, S3Error> {
216 self.send_request(
217 Command::PutObject {
218 content,
219 headers: extra_headers,
220 multipart: None,
221 },
222 path.as_ref(),
223 )
224 .await
225 }
226
227 pub async fn put_stream<R>(
229 &self,
230 reader: &mut R,
231 path: String,
232 ) -> Result<PutStreamResponse, S3Error>
233 where
234 R: AsyncRead + Unpin,
235 {
236 self.put_stream_with_content_type(reader, path, "application/octet-stream".to_string())
237 .await
238 }
239
240 async fn initiate_multipart_upload(
241 &self,
242 path: &str,
243 extra_headers: HeaderMap,
244 ) -> Result<InitiateMultipartUploadResponse, S3Error> {
245 let res = self
246 .send_request(
247 Command::InitiateMultipartUpload {
248 headers: extra_headers,
249 },
250 path,
251 )
252 .await?;
253 Ok(quick_xml::de::from_str(&res.text().await?)?)
254 }
255
256 async fn multipart_request(
257 &self,
258 path: &str,
259 chunk: Vec<u8>,
260 part_number: u32,
261 upload_id: &str,
262 ) -> Result<Response, S3Error> {
263 self.send_request(
264 Command::PutObject {
265 content: &chunk,
267 multipart: Some(Multipart::new(part_number, upload_id)),
268 headers: HeaderMap::new(),
269 },
270 path,
271 )
272 .await
273 }
274
275 async fn complete_multipart_upload(
276 &self,
277 path: &str,
278 upload_id: &str,
279 parts: Vec<Part>,
280 ) -> Result<Response, S3Error> {
281 let data = CompleteMultipartUploadData { parts };
282 self.send_request(Command::CompleteMultipartUpload { upload_id, data }, path)
283 .await
284 }
285
286 #[tracing::instrument(level = "debug", skip_all, fields(path = path))]
288 pub async fn put_stream_with_content_type<R>(
289 &self,
290 reader: &mut R,
291 path: String,
292 content_type: String,
293 ) -> Result<PutStreamResponse, S3Error>
294 where
295 R: AsyncRead + Unpin,
296 {
297 let mut headers = HeaderMap::new();
298 headers.insert(CONTENT_TYPE, HeaderValue::from_str(&content_type)?);
299
300 self.put_stream_with(reader, path, headers).await
301 }
302
303 #[tracing::instrument(level = "debug", skip_all, fields(path = path))]
309 pub async fn put_stream_with<R>(
310 &self,
311 reader: &mut R,
312 path: String,
313 extra_headers: HeaderMap,
314 ) -> Result<PutStreamResponse, S3Error>
315 where
316 R: AsyncRead + Unpin,
317 {
318 let mut first_chunk = Vec::with_capacity(CHUNK_SIZE);
323 let first_chunk_size = reader
324 .take(CHUNK_SIZE as u64)
325 .read_to_end(&mut first_chunk)
326 .await?;
327
328 debug!("first_chunk size: {}", first_chunk.len());
329 if first_chunk_size < CHUNK_SIZE {
330 debug!("first_chunk_size < CHUNK_SIZE -> doing normal PUT without stream");
331 let res = self
332 .put_with(&path, first_chunk.as_slice(), extra_headers)
333 .await;
334
335 return match res {
336 Ok(res) => Ok(PutStreamResponse {
337 status_code: res.status().as_u16(),
338 uploaded_bytes: first_chunk_size,
339 }),
340 Err(err) => Err(err),
341 };
342 }
343
344 debug!("first_chunk_size > CHUNK_SIZE -> initiate streaming upload");
345
346 let (tx, rx) = flume::bounded(2);
354
355 let slf = self.clone();
357 let handle_writer = tokio::spawn(async move {
358 debug!("writer task has been started");
359
360 let msg = slf.initiate_multipart_upload(&path, extra_headers).await?;
361 debug!("{:?}", msg);
362 if msg.upload_id.is_empty() {
366 return Err(S3Error::UnexpectedResponse(
367 "empty UploadId in InitiateMultipartUpload response",
368 ));
369 }
370 if msg.key != path {
371 warn!(
372 "server echoed a different Key (\"{}\" != \"{}\"); continuing against the requested key",
373 msg.key, path
374 );
375 }
376 let upload_id = &msg.upload_id;
380
381 let mut part_number: u32 = 0;
382 let mut etags = Vec::new();
383
384 let mut total_size = 0;
385 loop {
386 let chunk = if part_number == 0 {
387 let mut bytes = Vec::default();
389 mem::swap(&mut first_chunk, &mut bytes);
390 bytes
391 } else {
392 match rx.recv_async().await {
393 Ok(StreamChunk::Data(chunk)) => chunk,
394 Ok(StreamChunk::End) => {
395 debug!("no more parts available in reader - finishing upload");
396 break;
397 }
398 Ok(StreamChunk::Error(err)) => {
399 slf.abort_upload(&path, upload_id).await?;
403 return Err(S3Error::Io(err));
404 }
405 Err(err) => {
406 debug!("chunk reader channel has been closed: {}", err);
407 break;
408 }
409 }
410 };
411 debug!("chunk size in loop {}: {}", part_number + 1, chunk.len());
412
413 total_size += chunk.len();
414
415 part_number += 1;
417 let res = slf
418 .multipart_request(&path, chunk, part_number, upload_id)
419 .await;
420
421 match res {
422 Ok(res) => {
423 let etag = res
424 .headers()
425 .get("etag")
426 .ok_or_else(|| {
427 S3Error::UnexpectedResponse(
428 "missing ETag in multipart response headers",
429 )
430 })?
431 .to_str()
432 .map_err(S3Error::HeaderToStr)?;
433 etags.push(etag.to_string());
434 }
435 Err(err) => {
436 slf.abort_upload(&path, upload_id).await?;
438 return Err(err);
439 }
440 }
441 }
442 debug!(
443 "multipart uploading finished after {} parts with total size of {} bytes",
444 part_number, total_size
445 );
446
447 let inner_data = etags
449 .into_iter()
450 .enumerate()
451 .map(|(i, etag)| Part {
452 etag,
453 part_number: i as u32 + 1,
454 })
455 .collect::<Vec<Part>>();
456 debug!("data for multipart finishing: {:?}", inner_data);
457 let res = slf
458 .complete_multipart_upload(&path, &msg.upload_id, inner_data)
459 .await;
460
461 match res {
462 Ok(res) => Ok(PutStreamResponse {
463 status_code: res.status().as_u16(),
464 uploaded_bytes: total_size,
465 }),
466 Err(err) => Err(err),
467 }
468 });
469
470 loop {
472 let mut buf = Vec::with_capacity(CHUNK_SIZE);
473 match reader.take(CHUNK_SIZE as u64).read_to_end(&mut buf).await {
474 Ok(size) => {
475 if size == 0 {
476 debug!("stream reader finished reading");
477 if let Err(err) = tx.send_async(StreamChunk::End).await {
478 error!("sending the 'no more data' message in reader: {}", err);
479 }
480 break;
481 }
482
483 debug!("stream reader read {} bytes", size);
484 if let Err(err) = tx.send_async(StreamChunk::Data(buf)).await {
485 warn!(
486 "Stream Writer has been closed before reader finished: {}",
487 err
488 );
489 break;
490 }
491 }
492 Err(err) => {
493 error!("stream reader error: {}", err);
494 if let Err(send_err) = tx.send_async(StreamChunk::Error(err)).await {
498 error!(
499 "sending the read error to the writer failed: {}",
500 send_err
501 );
502 }
503 break;
504 }
505 }
506 }
507
508 handle_writer.await?
509 }
510
511 async fn list_page(
512 &self,
513 prefix: &str,
514 delimiter: Option<&str>,
515 continuation_token: Option<String>,
516 start_after: Option<String>,
517 max_keys: Option<usize>,
518 ) -> Result<ListBucketResult, S3Error> {
519 let command = if self.list_objects_v2 {
520 Command::ListObjectsV2 {
521 prefix,
522 delimiter,
523 continuation_token,
524 start_after,
525 max_keys,
526 }
527 } else {
528 Command::ListObjects {
532 prefix,
533 delimiter,
534 marker: std::cmp::max(continuation_token, start_after),
535 max_keys,
536 }
537 };
538
539 let resp = self.send_request(command, "/").await?;
540 let bytes = resp.bytes().await?;
541 let list_bucket_result = quick_xml::de::from_reader(bytes.as_ref())?;
542 Ok(list_bucket_result)
543 }
544
545 pub async fn list(
547 &self,
548 prefix: &str,
549 delimiter: Option<&str>,
550 ) -> Result<Vec<ListBucketResult>, S3Error> {
551 let mut results = Vec::new();
552 let mut continuation_token: Option<String> = None;
553
554 loop {
555 let list_bucket_result = self
556 .list_page(prefix, delimiter, continuation_token.clone(), None, None)
557 .await?;
558 let next_token = list_bucket_result.next_continuation_token.clone();
559 results.push(list_bucket_result);
560
561 match next_token {
562 Some(token) if continuation_token.as_deref() == Some(token.as_str()) => {
567 debug!("list pagination made no progress (repeated continuation token) - stopping");
568 break;
569 }
570 Some(token) => {
571 continuation_token = Some(token);
572 }
573 None => break,
574 }
575 }
576
577 Ok(results)
578 }
579
580 pub async fn copy_internal<F, T>(&self, from: F, to: T) -> Result<S3StatusCode, S3Error>
582 where
583 F: AsRef<str>,
584 T: AsRef<str>,
585 {
586 self.copy_internal_with(from, to, HeaderMap::new()).await
587 }
588
589 pub async fn copy_internal_with<F, T>(
611 &self,
612 from: F,
613 to: T,
614 extra_headers: HeaderMap,
615 ) -> Result<S3StatusCode, S3Error>
616 where
617 F: AsRef<str>,
618 T: AsRef<str>,
619 {
620 let fq_from = build_copy_source(&self.name, from.as_ref());
621 Ok(self
622 .send_request(
623 Command::CopyObject {
624 from: &fq_from,
625 headers: extra_headers,
626 },
627 to.as_ref(),
628 )
629 .await?
630 .status())
631 }
632
633 pub async fn copy_internal_from<B, F, T>(
635 &self,
636 from_bucket: B,
637 from_object: F,
638 to: T,
639 ) -> Result<S3StatusCode, S3Error>
640 where
641 B: AsRef<str>,
642 F: AsRef<str>,
643 T: AsRef<str>,
644 {
645 let fq_from = build_copy_source(from_bucket.as_ref(), from_object.as_ref());
646 Ok(self
647 .send_request(
648 Command::CopyObject {
649 from: &fq_from,
650 headers: HeaderMap::new(),
651 },
652 to.as_ref(),
653 )
654 .await?
655 .status())
656 }
657
658 async fn abort_upload(&self, key: &str, upload_id: &str) -> Result<(), S3Error> {
659 let resp = self
660 .send_request(Command::AbortMultipartUpload { upload_id }, key)
661 .await?;
662
663 let status = resp.status();
664 if status.is_success() {
665 Ok(())
666 } else {
667 let utf8_content = String::from_utf8(resp.bytes().await?.to_vec())?;
668 Err(S3Error::HttpFailWithBody(status.as_u16(), utf8_content))
669 }
670 }
671
672 async fn send_request(
673 &self,
674 mut command: Command<'_>,
675 path: &str,
676 ) -> Result<reqwest::Response, S3Error> {
677 let url = self.build_url(&command, path)?;
678 let headers = self.build_headers(&mut command, &url).await?;
679
680 let builder = Self::get_client()
681 .request(command.http_method(), url)
682 .headers(headers);
683
684 let res = match command {
685 Command::PutObject { content, .. } => builder.body(content.to_vec()),
686 Command::PutObjectTagging { tags } => builder.body(tags.to_string()),
687 Command::UploadPart { content, .. } => builder.body(content.to_vec()),
688 Command::CompleteMultipartUpload { ref data, .. } => {
689 let body = data.to_string();
690 builder.body(body)
691 }
692 _ => builder.body(Vec::default()),
693 }
694 .send()
695 .await?;
696
697 if res.status().is_success() {
698 Ok(res)
699 } else {
700 Err(S3Error::HttpFailWithBody(
701 res.status().as_u16(),
702 res.text().await?,
703 ))
704 }
705 }
706
707 fn get_client<'a>() -> &'a reqwest::Client {
708 CLIENT.get_or_init(|| {
709 #[allow(unused_mut)]
710 let mut builder = reqwest::Client::builder()
711 .brotli(true)
712 .connect_timeout(Duration::from_secs(10))
713 .tcp_keepalive(Duration::from_secs(30))
714 .pool_idle_timeout(Duration::from_secs(600));
715
716 #[cfg(any(
717 feature = "rustls",
718 feature = "rustls-no-provider",
719 feature = "native-tls",
720 feature = "native-tls-no-alpn",
721 feature = "native-tls-vendored",
722 feature = "native-tls-vendored-no-alpn"
723 ))]
724 {
725 let danger_accept_invalid =
726 env::var("S3_DANGER_ALLOW_INSECURE").as_deref() == Ok("true");
727
728 builder = builder
729 .tls_danger_accept_invalid_certs(danger_accept_invalid)
730 .tls_version_min(reqwest::tls::Version::TLS_1_2);
731 }
732
733 #[cfg(feature = "webpki-roots")]
734 {
735 builder = builder.tls_certs_merge(
736 webpki_root_certs::TLS_SERVER_ROOT_CERTS
737 .iter()
738 .map(|c| reqwest::Certificate::from_der(c).unwrap()),
739 );
740 }
741
742 builder.build().unwrap()
743 })
744 }
745
746 async fn build_headers(
751 &self,
752 command: &mut Command<'_>,
753 url: &Url,
754 ) -> Result<HeaderMap, S3Error> {
755 let cmd_hash = command.sha256();
756 let now = OffsetDateTime::now_utc();
757
758 let mut headers = match command {
760 Command::PutObject { headers, .. }
761 | Command::InitiateMultipartUpload { headers, .. }
762 | Command::CopyObject { headers, .. } => std::mem::take(headers),
763 _ => HeaderMap::with_capacity(4),
764 };
765
766 let domain = self.host_domain();
768 if self.path_style {
769 headers.insert(HOST, HeaderValue::from_str(domain.as_str())?);
770 } else {
771 headers.insert(
772 HOST,
773 HeaderValue::try_from(format!("{}.{}", self.name, domain))?,
774 );
775 }
776
777 match command {
779 Command::CopyObject { from, .. } => {
780 headers.insert(
781 HeaderName::from_static("x-amz-copy-source"),
782 HeaderValue::from_str(from)?,
783 );
784 }
785 Command::ListObjects { .. } => {}
786 Command::ListObjectsV2 { .. } => {}
787 Command::GetObject => {}
788 Command::GetObjectTagging => {}
789 Command::GetBucketLocation => {}
790
791 Command::InitiateMultipartUpload { .. } => {
792 if !headers.contains_key(CONTENT_TYPE) {
793 headers.insert(
794 CONTENT_TYPE,
795 HeaderValue::from_str("application/octet-stream")?,
796 );
797 }
798 }
799 Command::CompleteMultipartUpload { .. } => {
800 headers.insert(CONTENT_TYPE, HeaderValue::from_str("application/xml")?);
801 }
802 Command::PutObject { multipart, .. } => {
803 if multipart.is_none() && !headers.contains_key(CONTENT_TYPE) {
808 headers.insert(
809 CONTENT_TYPE,
810 HeaderValue::from_str("application/octet-stream")?,
811 );
812 }
813 }
814
815 Command::DeleteObject => {}
818 Command::GetObjectRange { .. } => {}
819 Command::HeadObject { .. } => {}
820
821 _ => {
822 headers.insert(
823 CONTENT_LENGTH,
824 HeaderValue::try_from(command.content_length().to_string())?,
825 );
826 headers.insert(CONTENT_TYPE, HeaderValue::from_str("text/plain")?);
827 }
828 }
829
830 headers.insert(
832 HeaderName::from_static("x-amz-content-sha256"),
833 HeaderValue::from_str(&cmd_hash)?,
834 );
835 headers.insert(
836 HeaderName::from_static("x-amz-date"),
837 HeaderValue::try_from(now.format(LONG_DATE_TIME)?)?,
838 );
839
840 match command {
841 Command::PutObjectTagging { tags } => {
842 headers.insert(
843 HeaderName::from_static("content-md5"),
844 HeaderValue::try_from(md5_url_encode(tags.as_bytes()))?,
845 );
846 }
847 Command::PutObject { content, .. } => {
848 headers.insert(
849 HeaderName::from_static("content-md5"),
850 HeaderValue::try_from(md5_url_encode(content))?,
851 );
852 }
853 Command::UploadPart { content, .. } => {
854 headers.insert(
855 HeaderName::from_static("content-md5"),
856 HeaderValue::try_from(md5_url_encode(content))?,
857 );
858 }
859 Command::GetObject => {
860 headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream"));
861 }
862 Command::GetObjectRange { start, end } => {
863 headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream"));
864
865 let range = if let Some(end) = end {
866 format!("bytes={}-{}", start, end)
867 } else {
868 format!("bytes={}-", start)
869 };
870 headers.insert(RANGE, HeaderValue::try_from(range)?);
871 }
872 _ => {}
873 }
874
875 let canonical_request =
877 signature::canonical_request(&command.http_method(), url, &headers, &cmd_hash)?;
878 let string_to_sign =
879 signature::string_to_sign(&now, &self.region, canonical_request.as_bytes())?;
880 let signing_key =
881 signature::signing_key(&now, &self.credentials.access_key_secret, &self.region)?;
882 let mut hmac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
883 hmac.update(string_to_sign.as_bytes());
884 let signature = hex::encode(hmac.finalize().into_bytes());
885 let signed_header = signature::signed_header_string(&headers);
886 let authorization = signature::authorization_header(
887 &self.credentials.access_key_id,
888 &now,
889 &self.region,
890 &signed_header,
891 &signature,
892 )?;
893 headers.insert(AUTHORIZATION, HeaderValue::try_from(authorization)?);
894
895 headers.insert(DATE, HeaderValue::try_from(now.format(&Rfc2822)?)?);
902
903 Ok(headers)
904 }
905
906 fn build_url(&self, command: &Command, path: &str) -> Result<Url, S3Error> {
907 let mut url = if self.path_style {
908 format!(
909 "{}://{}/{}",
910 self.host.scheme(),
911 self.host_domain(),
912 self.name,
913 )
914 } else {
915 format!(
916 "{}://{}.{}",
917 self.host.scheme(),
918 self.name,
919 self.host_domain(),
920 )
921 };
922
923 let path = if let Some(stripped) = path.strip_prefix('/') {
924 stripped
925 } else {
926 path
927 };
928
929 url.push('/');
930 url.push_str(&signature::uri_encode(path, false));
931
932 match command {
933 Command::InitiateMultipartUpload { .. } | Command::ListMultipartUploads { .. } => {
934 url.push_str("?uploads")
935 }
936 Command::AbortMultipartUpload { upload_id } => {
937 write!(url, "?uploadId={}", signature::uri_encode(upload_id, false))
940 .expect("write! to succeed");
941 }
942 Command::CompleteMultipartUpload { upload_id, .. } => {
943 write!(url, "?uploadId={}", signature::uri_encode(upload_id, false))
944 .expect("write! to succeed");
945 }
946 Command::PutObject {
947 multipart: Some(multipart),
948 ..
949 } => url.push_str(&multipart.query_string()),
950 _ => {}
951 }
952
953 let mut url = Url::parse(&url)?;
954
955 match command {
956 Command::ListObjectsV2 {
957 prefix,
958 delimiter,
959 continuation_token,
960 start_after,
961 max_keys,
962 } => {
963 let mut query_pairs = url.query_pairs_mut();
964 if let Some(d) = delimiter {
965 query_pairs.append_pair("delimiter", d);
966 }
967
968 query_pairs.append_pair("prefix", prefix);
969 query_pairs.append_pair("list-type", "2");
970 if let Some(token) = continuation_token {
971 query_pairs.append_pair("continuation-token", token);
972 }
973 if let Some(start_after) = start_after {
974 query_pairs.append_pair("start-after", start_after);
975 }
976 if let Some(max_keys) = max_keys {
977 query_pairs.append_pair("max-keys", &max_keys.to_string());
978 }
979 }
980
981 Command::ListObjects {
982 prefix,
983 delimiter,
984 marker,
985 max_keys,
986 } => {
987 let mut query_pairs = url.query_pairs_mut();
988 if let Some(d) = delimiter {
989 query_pairs.append_pair("delimiter", d);
990 }
991
992 query_pairs.append_pair("prefix", prefix);
993 if let Some(marker) = marker {
994 query_pairs.append_pair("marker", marker);
995 }
996 if let Some(max_keys) = max_keys {
997 query_pairs.append_pair("max-keys", &max_keys.to_string());
998 }
999 }
1000
1001 Command::ListMultipartUploads {
1002 prefix,
1003 delimiter,
1004 key_marker,
1005 max_uploads,
1006 } => {
1007 let mut query_pairs = url.query_pairs_mut();
1008 delimiter.map(|d| query_pairs.append_pair("delimiter", d));
1009 if let Some(prefix) = prefix {
1010 query_pairs.append_pair("prefix", prefix);
1011 }
1012 if let Some(key_marker) = key_marker {
1013 query_pairs.append_pair("key-marker", key_marker);
1014 }
1015 if let Some(max_uploads) = max_uploads {
1016 query_pairs.append_pair("max-uploads", max_uploads.to_string().as_str());
1017 }
1018 }
1019
1020 Command::PutObjectTagging { .. }
1021 | Command::GetObjectTagging
1022 | Command::DeleteObjectTagging => {
1023 url.query_pairs_mut().append_pair("tagging", "");
1024 }
1025
1026 _ => {}
1027 }
1028
1029 Ok(url)
1030 }
1031}
1032
1033fn build_copy_source(bucket: &str, from_object: &str) -> String {
1040 let from_object = from_object.strip_prefix('/').unwrap_or(from_object);
1041 format!("{}/{}", bucket, signature::uri_encode(from_object, false))
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047 use pretty_assertions::assert_eq;
1048 use tokio::fs;
1049 use tracing_test::traced_test;
1050
1051 #[test]
1052 fn test_build_copy_source_encodes_object_key() {
1053 assert_eq!(
1055 build_copy_source("my-bucket", "a b/c&d=é"),
1056 "my-bucket/a%20b/c%26d%3D%C3%A9"
1057 );
1058 assert_eq!(build_copy_source("b", "/key"), "b/key");
1060 }
1061
1062 #[test]
1063 fn build_url_encodes_server_provided_upload_id() {
1064 let bucket = Bucket::new(
1065 Url::parse("https://s3.example.com").unwrap(),
1066 "my-bucket".into(),
1067 crate::Region::new("us-east-1"),
1068 crate::credentials::Credentials::new("ak", "sk"),
1069 None,
1070 )
1071 .unwrap();
1072
1073 for command in [
1074 Command::AbortMultipartUpload {
1075 upload_id: "abc&partNumber=9".into(),
1076 },
1077 Command::CompleteMultipartUpload {
1078 upload_id: "abc&partNumber=9".into(),
1079 data: CompleteMultipartUploadData { parts: vec![] },
1080 },
1081 ] {
1082 let url = bucket.build_url(&command, "key").unwrap();
1083 assert!(url.as_str().ends_with("uploadId=abc%26partNumber%3D9"));
1086 }
1087 }
1088
1089 #[traced_test]
1090 #[tokio::test]
1091 async fn test_object_flow() -> Result<(), S3Error> {
1092 dotenvy::dotenv().ok().unwrap();
1093
1094 let bucket = Bucket::try_from_env().expect("env vars to be set in .env");
1095
1096 let file_sizes = vec![
1098 0,
1099 1,
1100 CHUNK_SIZE / 2,
1101 CHUNK_SIZE - 1,
1102 CHUNK_SIZE,
1103 CHUNK_SIZE + 1,
1104 ];
1105
1106 for file_size in file_sizes {
1107 println!("test_object_flow with {} bytes", file_size);
1108
1109 let _ = fs::create_dir_all("test_files").await;
1110 let file_name_input = format!("test_data_{}", file_size);
1111 let input_path = format!("test_files/{}", file_name_input);
1112 let file_name_output = format!("test_data_{}.out", file_size);
1113 let output_path = format!("test_files/{}", file_name_output);
1114
1115 let bytes = (0..file_size).into_iter().map(|_| 0u8).collect::<Vec<u8>>();
1117 fs::write(&input_path, &bytes).await?;
1118
1119 let res = bucket.put(&file_name_input, &bytes).await?;
1121 let status = res.status();
1122 let body = res.text().await?;
1123 println!("response body:\n{}", body);
1124 assert!(status.is_success());
1125
1126 tokio::time::sleep(Duration::from_secs(1)).await;
1128
1129 let res = bucket.get(&file_name_input).await?;
1131 assert!(res.status().is_success());
1132 let body = res.bytes().await?;
1133 assert_eq!(body.len(), file_size);
1134 fs::write(&output_path, body.as_ref()).await?;
1135
1136 let input_bytes = fs::read(input_path).await?;
1138 let output_bytes = fs::read(output_path).await?;
1139 assert_eq!(input_bytes.len(), file_size);
1140 assert_eq!(input_bytes.len(), output_bytes.len());
1141 assert_eq!(input_bytes, output_bytes);
1142
1143 let list = bucket.list(&bucket.name, None).await?;
1145 for entry in list.iter() {
1146 if entry.name == bucket.name {
1147 for object in entry.contents.iter() {
1148 if object.key == file_name_input {
1149 assert_eq!(object.size, file_size as u64);
1151 break;
1152 }
1153 }
1154 }
1155 }
1156
1157 let res = bucket.head(&file_name_input).await?;
1159 assert_eq!(res.content_length, Some(file_size as u64));
1160
1161 if file_size > CHUNK_SIZE / 2 {
1162 let end = CHUNK_SIZE / 2 + 1;
1164 let res = bucket
1165 .get_range(&file_name_input, 0, Some(end as u64))
1166 .await?;
1167 assert!(res.status().is_success());
1168 let body = res.bytes().await?;
1169 assert_eq!(body.len(), end + 1);
1171 }
1172
1173 let res = bucket
1175 .copy_internal(&file_name_input, &file_name_output)
1176 .await?;
1177 assert!(res.is_success());
1178
1179 let res = bucket.get(&file_name_output).await?;
1181 assert!(res.status().is_success());
1182 let body = res.bytes().await?;
1183 assert_eq!(body.len(), file_size);
1184
1185 let res = bucket.delete(&file_name_input).await?;
1187 assert!(res.status().is_success());
1188 let res = bucket.delete(&file_name_output).await?;
1189 assert!(res.status().is_success());
1190
1191 let list = bucket.list(&bucket.name, None).await?;
1193 for entry in list.iter() {
1194 if entry.name == bucket.name {
1195 for object in entry.contents.iter() {
1196 if object.key == file_name_input {
1197 panic!("test file has not been deleted");
1198 }
1199 }
1200 }
1201 }
1202 }
1203
1204 Ok(())
1205 }
1206
1207 #[traced_test]
1208 #[tokio::test]
1209 async fn test_multipart() -> Result<(), S3Error> {
1210 use futures_util::stream::StreamExt;
1211 use std::os::unix::fs::MetadataExt;
1212 use tokio::io::AsyncWriteExt;
1213
1214 dotenvy::dotenv().ok().unwrap();
1215 let bucket = Bucket::try_from_env().expect("env vars to be set in .env");
1216
1217 let file_sizes = vec![
1219 CHUNK_SIZE - 1,
1220 CHUNK_SIZE,
1221 CHUNK_SIZE + 1,
1222 CHUNK_SIZE * 2,
1223 CHUNK_SIZE * 3,
1224 CHUNK_SIZE * 3 + 1,
1225 ];
1226
1227 for file_size in file_sizes {
1228 let _ = fs::create_dir_all("test_files").await;
1230 let file_name_input = format!("test_data_mp_{}", file_size);
1231 let input_path = format!("test_files/{}", file_name_input);
1232 let file_name_output = format!("test_data_mp_{}.out", file_size);
1233 let output_path = format!("test_files/{}", file_name_output);
1234
1235 let bytes = (0..file_size).into_iter().map(|_| 0u8).collect::<Vec<u8>>();
1236 fs::write(&input_path, &bytes).await?;
1237
1238 let mut reader_file = fs::File::open(&input_path).await?;
1240 let res = bucket
1241 .put_stream(&mut reader_file, file_name_input.clone())
1242 .await?;
1243 assert!(res.status_code < 300);
1244 assert_eq!(res.uploaded_bytes, file_size);
1245
1246 let mut file = fs::File::create(&output_path).await?;
1248
1249 let res = bucket.get(&file_name_input).await?;
1250 assert!(res.status().is_success());
1251
1252 let stream = res.bytes_stream();
1253 tokio::pin!(stream);
1254 while let Some(Ok(item)) = stream.next().await {
1255 file.write_all(item.as_ref()).await?;
1256 }
1257 file.sync_all().await?;
1259
1260 let f_in = fs::File::open(&input_path).await?;
1262 let f_out = fs::File::open(&output_path).await?;
1263 let meta_in = f_in.metadata().await.unwrap();
1264 let meta_out = f_out.metadata().await.unwrap();
1265 assert_eq!(meta_in.size(), meta_out.size());
1266 }
1267
1268 Ok(())
1269 }
1270}