Skip to main content

s3_simple/
bucket.rs

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; // 8 MiB, min for S3 is 5MiB
29
30/// Messages passed from the streaming reader to the multipart writer task.
31enum 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)] // false-positive warnings
64impl Bucket {
65    fn host_domain(&self) -> String {
66        match self.host.domain() {
67            None => {
68                // in this case, we have an IP as part of the domain
69                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    /// HEAD information for an object
127    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    /// GET an object
135    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        // S3 byte ranges are inclusive on both ends, so `start == end` is a
149        // valid request for exactly one byte.
150        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    /// DELETE an object
158    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    /// PUT an object
164    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    /// PUT an object with a specific content type
170    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    /// PUT an object with specific headers.
191    ///
192    /// `headers` accepts additional headers to include in the request. Required headers for the
193    /// request (i.e. `Authorization`, `Content-Length`) don't need to be included, as they are
194    /// still handled automatically.
195    ///
196    /// # Examples
197    ///
198    /// ```no_run
199    /// let bucket = Bucket::try_from_env().await?;
200    /// let mut headers = HeaderMap::new();
201    /// let content = b"world";
202    ///
203    /// // Denote that this a text file.
204    /// headers.insert("Content-Type", "text/plain");
205    /// // Tell S3 what the caching behavior this object should respond to clients with.
206    /// headers.insert("Cache-Control", "public, max-age=3600");
207    ///
208    /// bucket.put_with("hello.txt", content, headers).await?;
209    /// ```
210    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    /// Streaming object upload from any reader that implements `AsyncRead`
228    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                // TODO switch to owned data would make sense here probably
266                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    /// Streaming object upload from any reader that implements `AsyncRead`
287    #[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    /// Streaming object upload from any reader that implements [`AsyncRead`].
304    ///
305    /// `headers` accepts additional headers to include in the request. Required headers for the
306    /// request (i.e. `Authorization`, `Content-Length`) don't need to be included, as they are
307    /// still handled automatically.
308    #[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        // Grab the content type.
319
320        // If the file is smaller CHUNK_SIZE, just do a regular upload,
321        // Otherwise, perform a multipart upload.
322        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        // At this point, the file exceeds the CHUNK_SIZE.
347        // This means we will upload at least 2 chunks.
348        // To optimize the performance, the writer will be spawned on a dedicated
349        // tokio top level tasks to make optimal use of multiple cores.
350        // The very little cloned data is worth it to get better throughput.
351        // A channel with 2-chunk buffer will be used for the communication to
352        // get optimal performance out of the slower in / out pipelines.
353        let (tx, rx) = flume::bounded(2);
354
355        // Writer task
356        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            // The UploadId is server-generated and opaque, so there is no local
363            // value to compare it against; an empty one would silently corrupt
364            // every subsequent part request, so fail early.
365            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            // Upload the parts and complete against the *requested* key, not
377            // the server-echoed one: a buggy or hostile server could otherwise
378            // redirect where the object lands.
379            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                    // this memory swap avoids a clone of the first chunk
388                    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                            // The source stream failed mid-upload. Abort the
400                            // multipart upload and propagate the error instead
401                            // of blocking on the channel forever.
402                            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                // chunk upload
416                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                        // if chunk upload failed - abort the upload
437                        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            // Finish the upload
448            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        // The reader will run in this task for simplifying lifetimes
471        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                    // Tell the writer that the stream failed so it can abort
495                    // the upload instead of waiting for data that will never
496                    // come (which used to deadlock the whole call).
497                    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            // In the v1 ListObjects request, there is only one "marker"
529            // field that serves as both the initial starting position,
530            // and as the continuation token.
531            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    /// List bucket contents
546    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                // A repeated token means the server will hand us the same page
563                // again: stop instead of looping forever. Without this guard a
564                // hostile or buggy server could DoS the caller with an
565                // unbounded results vector.
566                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    /// S3 internal copy an object from one place to another inside the same bucket
581    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    /// S3 internal copy an object from one place to another inside the same bucket.
590    ///
591    /// `headers` accepts additional headers to include in the request. Required headers for the
592    /// request (i.e. `Authorization`, `Content-Length`) don't need to be included, as they are
593    /// still handled automatically.
594    ///
595    /// # Examples
596    ///
597    /// This example shows how to modify the metadata of an existing object in S3.
598    ///
599    /// ```no_run
600    /// let bucket = Bucket::try_from_env().await?;
601    /// let mut headers = HeaderMap::new();
602    ///
603    /// // `x-amz-metadata-directive` tells S3 what to do with the existing object metadata.
604    /// headers.insert("x-amz-metadata-directive", "REPLACE");
605    /// headers.insert("Content-Type", "image/jpeg");
606    /// headers.insert("Cache-Control", "public, max-age=86400");
607    ///
608    /// bucket.copy_internal_with("cat.jpg", "cat.jpg", headers).await?;
609    /// ```
610    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    /// S3 internal copy an object from another bucket into "this" bucket
634    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    /// Builds headers for the request.
747    ///
748    /// `command` is `&mut` since this function will consume any `headers` that were passed in from
749    /// the client.
750    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        // For commands that accept the `HeaderMap` as part of the command, re-use the map.
759        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        // host header
767        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        // add command specific header
778        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 this is not a multipart upload, default to `application/octet-stream` in case
804                // the content type was never set.
805                //
806                // N.B.: For multipart uploads, the content type is set during initiation.
807                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            // Needed to make Garage work while Minio
816            // seems to ignore `content-length: 0` for these
817            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        // hash and date
831        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        // sign all the above heavers with the secret
876        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        // The format of RFC2822 is somewhat malleable, so including it in
896        // signed headers can cause signature mismatches. We do include the
897        // X-Amz-Date header, so requests are still properly limited to a date
898        // range and can't be used again e.g. reply attacks. Adding this header
899        // after the generation of the Authorization header leaves it out of
900        // the signed headers.
901        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                // The UploadId is server-provided; encode it so a hostile value
938                // cannot inject query parameters into the request.
939                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
1033/// Build the `x-amz-copy-source` value for a copy request.
1034///
1035/// The S3 API requires the source object key to be URI-encoded (with `/`
1036/// preserved as the path separator). Without encoding, characters such as
1037/// spaces, non-ASCII bytes or `&` in the key would corrupt the header or
1038/// inject query parameters into the copy source.
1039fn 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        // spaces, `&` and non-ASCII must be percent-encoded; `/` is preserved
1054        assert_eq!(
1055            build_copy_source("my-bucket", "a b/c&d=é"),
1056            "my-bucket/a%20b/c%26d%3D%C3%A9"
1057        );
1058        // a leading slash is stripped before encoding
1059        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            // The UploadId must be percent-encoded so a hostile value cannot
1084            // inject query parameters into the request.
1085            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        // we do not use rstest here since the tests start multiple conflicting runtimes
1097        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            // create and write some test data
1116            let bytes = (0..file_size).into_iter().map(|_| 0u8).collect::<Vec<u8>>();
1117            fs::write(&input_path, &bytes).await?;
1118
1119            // upload the file
1120            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            // give the s3 replication under the hood a second
1127            tokio::time::sleep(Duration::from_secs(1)).await;
1128
1129            // GET the file back
1130            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            // make sure input and output are the same
1137            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            // list bucket content and make sure it shows up
1144            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                            // we found our dummy object, check the size
1150                            assert_eq!(object.size, file_size as u64);
1151                            break;
1152                        }
1153                    }
1154                }
1155            }
1156
1157            // validate that HEAD is working too
1158            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                // get only a part of the object back
1163                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                // the GET range included the end -> 1 additional byte
1170                assert_eq!(body.len(), end + 1);
1171            }
1172
1173            // test internal object copy
1174            let res = bucket
1175                .copy_internal(&file_name_input, &file_name_output)
1176                .await?;
1177            assert!(res.is_success());
1178
1179            // GET the new copied object
1180            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            // clean up and delete the test file
1186            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            // list bucket content again and make sure its gone
1192            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        // we do not use rstest here since the tests seem to interfere with each other on the IO layer
1218        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            // create and write some test data
1229            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            // streaming upload
1239            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            // streaming download
1247            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            // flush / sync all possibly left over data
1258            file.sync_all().await?;
1259
1260            // make sure the files match
1261            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}