@@ -1670,7 +1670,7 @@ impl Bucket {
16701670 }
16711671
16721672 let msg = self
1673- . initiate_multipart_upload ( s3_path, content_type)
1673+ . initiate_multipart_upload_with_headers ( s3_path, content_type, custom_headers )
16741674 . await ?;
16751675 let path = msg. key ;
16761676 let upload_id = & msg. upload_id ;
@@ -1866,8 +1866,55 @@ impl Bucket {
18661866 s3_path : & str ,
18671867 content_type : & str ,
18681868 ) -> Result < InitiateMultipartUploadResponse , S3Error > {
1869+ self . initiate_multipart_upload_with_headers ( s3_path, content_type, None )
1870+ . await
1871+ }
1872+
1873+ /// Initiate a multipart upload with headers that configure the completed object.
1874+ ///
1875+ /// Headers such as object metadata, cache control, storage class, and server-side
1876+ /// encryption using S3-managed or KMS keys must be supplied when the multipart upload is
1877+ /// initiated.
1878+ ///
1879+ /// # Example
1880+ ///
1881+ /// ```no_run
1882+ /// use anyhow::Result;
1883+ /// use http::{HeaderMap, HeaderValue};
1884+ /// use s3::{Bucket, creds::Credentials};
1885+ ///
1886+ /// # #[tokio::main]
1887+ /// # async fn main() -> Result<()> {
1888+ /// let bucket = Bucket::new("my-bucket", "us-east-1".parse()?, Credentials::default()?)?;
1889+ /// let mut headers = HeaderMap::new();
1890+ /// headers.insert(
1891+ /// "x-amz-meta-uploaded-by",
1892+ /// HeaderValue::from_static("multipart-client"),
1893+ /// );
1894+ ///
1895+ /// bucket
1896+ /// .initiate_multipart_upload_with_headers(
1897+ /// "/large-file.zip",
1898+ /// "application/zip",
1899+ /// Some(headers),
1900+ /// )
1901+ /// .await?;
1902+ /// # Ok(())
1903+ /// # }
1904+ /// ```
1905+ #[ maybe_async:: async_impl]
1906+ pub async fn initiate_multipart_upload_with_headers (
1907+ & self ,
1908+ s3_path : & str ,
1909+ content_type : & str ,
1910+ custom_headers : Option < HeaderMap > ,
1911+ ) -> Result < InitiateMultipartUploadResponse , S3Error > {
1912+ let mut request_bucket = self . clone ( ) ;
1913+ if let Some ( custom_headers) = custom_headers {
1914+ request_bucket. extra_headers . extend ( custom_headers) ;
1915+ }
18691916 let command = Command :: InitiateMultipartUpload { content_type } ;
1870- let request = RequestImpl :: new ( self , s3_path, command) . await ?;
1917+ let request = RequestImpl :: new ( & request_bucket , s3_path, command) . await ?;
18711918 let response_data = request. response_data ( false ) . await ?;
18721919 if response_data. status_code ( ) >= 300 {
18731920 return Err ( error_from_response_data ( response_data) ?) ;
@@ -1884,8 +1931,27 @@ impl Bucket {
18841931 s3_path : & str ,
18851932 content_type : & str ,
18861933 ) -> Result < InitiateMultipartUploadResponse , S3Error > {
1934+ self . initiate_multipart_upload_with_headers ( s3_path, content_type, None )
1935+ }
1936+
1937+ /// Initiate a multipart upload with headers that configure the completed object.
1938+ ///
1939+ /// Headers such as object metadata, cache control, storage class, and server-side
1940+ /// encryption using S3-managed or KMS keys must be supplied when the multipart upload is
1941+ /// initiated.
1942+ #[ maybe_async:: sync_impl]
1943+ pub fn initiate_multipart_upload_with_headers (
1944+ & self ,
1945+ s3_path : & str ,
1946+ content_type : & str ,
1947+ custom_headers : Option < HeaderMap > ,
1948+ ) -> Result < InitiateMultipartUploadResponse , S3Error > {
1949+ let mut request_bucket = self . clone ( ) ;
1950+ if let Some ( custom_headers) = custom_headers {
1951+ request_bucket. extra_headers . extend ( custom_headers) ;
1952+ }
18871953 let command = Command :: InitiateMultipartUpload { content_type } ;
1888- let request = RequestImpl :: new ( self , s3_path, command) ?;
1954+ let request = RequestImpl :: new ( & request_bucket , s3_path, command) ?;
18891955 let response_data = request. response_data ( false ) ?;
18901956 if response_data. status_code ( ) >= 300 {
18911957 return Err ( error_from_response_data ( response_data) ?) ;
@@ -3187,6 +3253,92 @@ mod test {
31873253 assert_eq ! ( requests. load( Ordering :: SeqCst ) , 1 ) ;
31883254 }
31893255
3256+ #[ cfg( all( not( feature = "sync" ) , feature = "with-tokio" ) ) ]
3257+ #[ tokio:: test]
3258+ async fn multipart_initiation_with_headers_sends_and_signs_object_configuration ( ) {
3259+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . unwrap ( ) ;
3260+ let endpoint = format ! ( "http://{}" , listener. local_addr( ) . unwrap( ) ) ;
3261+
3262+ let server = thread:: spawn ( move || {
3263+ let ( mut stream, _) = listener. accept ( ) . unwrap ( ) ;
3264+ let mut request_bytes = Vec :: new ( ) ;
3265+ let mut buffer = [ 0 ; 4096 ] ;
3266+
3267+ loop {
3268+ let bytes_read = stream. read ( & mut buffer) . unwrap ( ) ;
3269+ assert ! ( bytes_read > 0 , "connection closed before headers arrived" ) ;
3270+ request_bytes. extend_from_slice ( & buffer[ ..bytes_read] ) ;
3271+ if request_bytes. windows ( 4 ) . any ( |window| window == b"\r \n \r \n " ) {
3272+ break ;
3273+ }
3274+ }
3275+
3276+ let request = String :: from_utf8 ( request_bytes)
3277+ . unwrap ( )
3278+ . to_ascii_lowercase ( ) ;
3279+ assert ! ( request. starts_with( "post /test-bucket/artifact?uploads http/1.1\r \n " ) ) ;
3280+ assert ! ( request. contains( "\r \n x-amz-meta-artifact-tag: synthetic-signature\r \n " ) ) ;
3281+ assert ! ( request. contains( "\r \n cache-control: max-age=60\r \n " ) ) ;
3282+ assert ! ( request. contains( "\r \n x-amz-server-side-encryption: aes256\r \n " ) ) ;
3283+
3284+ let authorization = request
3285+ . lines ( )
3286+ . find ( |line| line. starts_with ( "authorization:" ) )
3287+ . unwrap ( ) ;
3288+ assert ! ( authorization. contains( "cache-control" ) ) ;
3289+ assert ! ( authorization. contains( "x-amz-meta-artifact-tag" ) ) ;
3290+ assert ! ( authorization. contains( "x-amz-server-side-encryption" ) ) ;
3291+
3292+ let body = "<InitiateMultipartUploadResult><Bucket>test-bucket</Bucket><Key>artifact</Key><UploadId>synthetic-upload-id</UploadId></InitiateMultipartUploadResult>" ;
3293+ let response = format ! (
3294+ "HTTP/1.1 200 OK\r \n Content-Type: application/xml\r \n Content-Length: {}\r \n Connection: close\r \n \r \n {body}" ,
3295+ body. len( )
3296+ ) ;
3297+ stream. write_all ( response. as_bytes ( ) ) . unwrap ( ) ;
3298+ } ) ;
3299+
3300+ let credentials = Credentials :: new (
3301+ Some ( "test_access_key" ) ,
3302+ Some ( "test_secret_key" ) ,
3303+ None ,
3304+ None ,
3305+ None ,
3306+ )
3307+ . unwrap ( ) ;
3308+ let bucket = Bucket :: new (
3309+ "test-bucket" ,
3310+ Region :: Custom {
3311+ region : "us-east-1" . to_owned ( ) ,
3312+ endpoint,
3313+ } ,
3314+ credentials,
3315+ )
3316+ . unwrap ( )
3317+ . with_path_style ( ) ;
3318+ let mut custom_headers = HeaderMap :: new ( ) ;
3319+ custom_headers. insert (
3320+ HeaderName :: from_static ( "x-amz-meta-artifact-tag" ) ,
3321+ HeaderValue :: from_static ( "synthetic-signature" ) ,
3322+ ) ;
3323+ custom_headers. insert ( CACHE_CONTROL , HeaderValue :: from_static ( "max-age=60" ) ) ;
3324+ custom_headers. insert (
3325+ HeaderName :: from_static ( "x-amz-server-side-encryption" ) ,
3326+ HeaderValue :: from_static ( "AES256" ) ,
3327+ ) ;
3328+
3329+ let initiated = bucket
3330+ . initiate_multipart_upload_with_headers (
3331+ "/artifact" ,
3332+ "application/octet-stream" ,
3333+ Some ( custom_headers) ,
3334+ )
3335+ . await
3336+ . unwrap ( ) ;
3337+
3338+ server. join ( ) . unwrap ( ) ;
3339+ assert_eq ! ( initiated. upload_id, "synthetic-upload-id" ) ;
3340+ }
3341+
31903342 #[ test]
31913343 #[ cfg( any( feature = "tokio-native-tls" , feature = "tokio-rustls-tls" ) ) ]
31923344 #[ allow( deprecated) ]
@@ -3595,6 +3747,71 @@ mod test {
35953747 streaming_test_put_get_delete_big_object ( * test_minio_bucket ( ) ) . await ;
35963748 }
35973749
3750+ #[ ignore]
3751+ #[ cfg( all( not( feature = "sync" ) , feature = "with-tokio" ) ) ]
3752+ #[ tokio:: test]
3753+ async fn streaming_minio_preserves_metadata_at_multipart_boundary_tokio ( ) {
3754+ streaming_minio_preserves_metadata_at_multipart_boundary ( ) . await ;
3755+ }
3756+
3757+ #[ ignore]
3758+ #[ cfg( all( not( feature = "sync" ) , feature = "with-async-std" ) ) ]
3759+ #[ async_std:: test]
3760+ async fn streaming_minio_preserves_metadata_at_multipart_boundary_async_std ( ) {
3761+ streaming_minio_preserves_metadata_at_multipart_boundary ( ) . await ;
3762+ }
3763+
3764+ #[ cfg( all(
3765+ not( feature = "sync" ) ,
3766+ any( feature = "with-tokio" , feature = "with-async-std" )
3767+ ) ) ]
3768+ async fn streaming_minio_preserves_metadata_at_multipart_boundary ( ) {
3769+ let bucket = test_minio_bucket ( ) ;
3770+ let artifact_tag = "synthetic-signature" ;
3771+ let sizes = [
3772+ crate :: bucket:: CHUNK_SIZE - 1 ,
3773+ crate :: bucket:: CHUNK_SIZE ,
3774+ crate :: bucket:: CHUNK_SIZE + 326_431 ,
3775+ ] ;
3776+
3777+ for size in sizes {
3778+ let remote_path = format ! ( "+stream_metadata_{}_{}" , size, uuid:: Uuid :: new_v4( ) ) ;
3779+ let content = vec ! [ 33 ; size] ;
3780+ #[ cfg( feature = "with-tokio" ) ]
3781+ let mut reader = std:: io:: Cursor :: new ( & content) ;
3782+ #[ cfg( feature = "with-async-std" ) ]
3783+ let mut reader = async_std:: io:: Cursor :: new ( & content) ;
3784+
3785+ let response = bucket
3786+ . put_object_stream_builder ( & remote_path)
3787+ . with_metadata ( "artifact-tag" , artifact_tag)
3788+ . unwrap ( )
3789+ . execute_stream ( & mut reader)
3790+ . await
3791+ . unwrap ( ) ;
3792+
3793+ assert_eq ! ( response. status_code( ) , 200 ) ;
3794+ assert_eq ! ( response. uploaded_bytes( ) , size) ;
3795+
3796+ let ( head, status) = bucket. head_object ( & remote_path) . await . unwrap ( ) ;
3797+ assert_eq ! ( status, 200 ) ;
3798+ assert_eq ! (
3799+ head. metadata
3800+ . as_ref( )
3801+ . and_then( |metadata| metadata. get( "artifact-tag" ) )
3802+ . map( String :: as_str) ,
3803+ Some ( artifact_tag)
3804+ ) ;
3805+
3806+ let downloaded = bucket. get_object ( & remote_path) . await . unwrap ( ) ;
3807+ assert_eq ! ( downloaded. status_code( ) , 200 ) ;
3808+ assert_eq ! ( downloaded. as_slice( ) , content) ;
3809+
3810+ let deleted = bucket. delete_object ( & remote_path) . await . unwrap ( ) ;
3811+ assert_eq ! ( deleted. status_code( ) , 204 ) ;
3812+ }
3813+ }
3814+
35983815 #[ ignore]
35993816 #[ maybe_async:: test(
36003817 feature = "sync" ,
0 commit comments