@@ -116,6 +116,20 @@ pub(crate) enum Error {
116116
117117 #[ error( "Upload aborted" ) ]
118118 Aborted ,
119+
120+ #[ cfg( feature = "xattr" ) ]
121+ #[ error( "Unable to set extended attribute on {}: {source}" , path. display( ) ) ]
122+ UnableToSetXattr {
123+ source : std:: io:: Error ,
124+ path : PathBuf ,
125+ } ,
126+
127+ #[ cfg( feature = "xattr" ) ]
128+ #[ error( "Unable to read extended attribute on {}: {source}" , path. display( ) ) ]
129+ UnableToReadXattr {
130+ source : std:: io:: Error ,
131+ path : PathBuf ,
132+ } ,
119133}
120134
121135impl From < Error > for super :: Error {
@@ -325,6 +339,103 @@ fn is_valid_file_path(path: &Path) -> bool {
325339 }
326340}
327341
342+ /// Sets extended attributes on a file from an Attributes collection
343+ #[ cfg( feature = "xattr" ) ]
344+ fn set_xattrs ( path : & std:: path:: Path , attributes : & Attributes ) -> Result < ( ) > {
345+ use crate :: Attribute ;
346+ use std:: borrow:: Cow ;
347+
348+ for ( attr, value) in attributes {
349+ let name: Cow < ' static , str > = match attr {
350+ Attribute :: CacheControl => Cow :: Borrowed ( "user.object_store.cache_control" ) ,
351+ Attribute :: ContentDisposition => Cow :: Borrowed ( "user.object_store.content_disposition" ) ,
352+ Attribute :: ContentEncoding => Cow :: Borrowed ( "user.object_store.content_encoding" ) ,
353+ Attribute :: ContentLanguage => Cow :: Borrowed ( "user.object_store.content_language" ) ,
354+ Attribute :: ContentType => Cow :: Borrowed ( "user.object_store.content_type" ) ,
355+ Attribute :: StorageClass => Cow :: Borrowed ( "user.object_store.storage_class" ) ,
356+ Attribute :: Metadata ( key) => Cow :: Owned ( format ! ( "user.object_store.{key}" ) ) ,
357+ } ;
358+ xattr:: set ( path, name. as_ref ( ) , value. as_ref ( ) . as_bytes ( ) ) . map_err ( |source| {
359+ Error :: UnableToSetXattr {
360+ source,
361+ path : path. into ( ) ,
362+ }
363+ } ) ?;
364+ }
365+ Ok ( ( ) )
366+ }
367+
368+ /// Reads extended attributes from a file and returns an Attributes collection
369+ #[ cfg( feature = "xattr" ) ]
370+ fn get_xattrs ( path : & std:: path:: Path ) -> Result < Attributes > {
371+ use crate :: Attribute ;
372+
373+ let mut attributes = Attributes :: new ( ) ;
374+
375+ let list = match xattr:: list ( path) {
376+ Ok ( list) => list,
377+ Err ( source) => {
378+ return Err ( Error :: UnableToReadXattr {
379+ source,
380+ path : path. into ( ) ,
381+ }
382+ . into ( ) ) ;
383+ }
384+ } ;
385+
386+ for name in list {
387+ let name_str = match name. to_str ( ) {
388+ Some ( s) if s. starts_with ( "user.object_store." ) => s,
389+ _ => continue ,
390+ } ;
391+
392+ let value = match xattr:: get ( path, & name) {
393+ Ok ( Some ( v) ) => match String :: from_utf8 ( v) {
394+ Ok ( s) => s,
395+ Err ( _) => continue ,
396+ } ,
397+ Ok ( None ) => continue ,
398+ Err ( source) => {
399+ return Err ( Error :: UnableToReadXattr {
400+ source,
401+ path : path. into ( ) ,
402+ }
403+ . into ( ) ) ;
404+ }
405+ } ;
406+
407+ let key = & name_str[ "user.object_store." . len ( ) ..] ;
408+ let attr = match key {
409+ "cache_control" => Attribute :: CacheControl ,
410+ "content_disposition" => Attribute :: ContentDisposition ,
411+ "content_encoding" => Attribute :: ContentEncoding ,
412+ "content_language" => Attribute :: ContentLanguage ,
413+ "content_type" => Attribute :: ContentType ,
414+ "storage_class" => Attribute :: StorageClass ,
415+ key => Attribute :: Metadata ( key. to_string ( ) . into ( ) ) ,
416+ } ;
417+ attributes. insert ( attr, value. into ( ) ) ;
418+ }
419+ Ok ( attributes)
420+ }
421+
422+ /// Returns an error if attributes are non-empty when xattr feature is disabled.
423+ #[ cfg( not( feature = "xattr" ) ) ]
424+ fn set_xattrs ( _path : & std:: path:: Path , attributes : & Attributes ) -> Result < ( ) > {
425+ if !attributes. is_empty ( ) {
426+ return Err ( super :: Error :: NotSupported {
427+ source : "Setting extended attributes requires the 'xattr' feature" . into ( ) ,
428+ } ) ;
429+ }
430+ Ok ( ( ) )
431+ }
432+
433+ /// No-op when xattr feature is disabled: returns empty attributes.
434+ #[ cfg( not( feature = "xattr" ) ) ]
435+ fn get_xattrs ( _path : & std:: path:: Path ) -> Result < Attributes > {
436+ Ok ( Attributes :: new ( ) )
437+ }
438+
328439#[ async_trait]
329440impl ObjectStore for LocalFileSystem {
330441 async fn put_opts (
@@ -340,13 +451,6 @@ impl ObjectStore for LocalFileSystem {
340451 } ) ;
341452 }
342453
343- if !opts. attributes . is_empty ( ) {
344- return Err ( crate :: Error :: NotImplemented {
345- operation : "`put_opts` with `opts.attributes` specified" . into ( ) ,
346- implementer : self . to_string ( ) ,
347- } ) ;
348- }
349-
350454 let path = self . path_to_filesystem ( location) ?;
351455 maybe_spawn_blocking ( move || {
352456 let ( mut file, staging_path) = new_staged_upload ( & path) ?;
@@ -359,6 +463,9 @@ impl ObjectStore for LocalFileSystem {
359463 path : path. to_string_lossy ( ) . to_string ( ) ,
360464 } ) ?;
361465 e_tag = Some ( get_etag ( & metadata) ) ;
466+
467+ set_xattrs ( & staging_path, & opts. attributes ) ?;
468+
362469 match opts. mode {
363470 PutMode :: Overwrite => {
364471 // For some fuse types of file systems, the file must be closed first
@@ -406,16 +513,9 @@ impl ObjectStore for LocalFileSystem {
406513 location : & Path ,
407514 opts : PutMultipartOptions ,
408515 ) -> Result < Box < dyn MultipartUpload > > {
409- if !opts. attributes . is_empty ( ) {
410- return Err ( crate :: Error :: NotImplemented {
411- operation : "`put_multipart_opts` with `opts.attributes` specified" . into ( ) ,
412- implementer : self . to_string ( ) ,
413- } ) ;
414- }
415-
416516 let dest = self . path_to_filesystem ( location) ?;
417517 let ( file, src) = new_staged_upload ( & dest) ?;
418- Ok ( Box :: new ( LocalUpload :: new ( src, dest, file) ) )
518+ Ok ( Box :: new ( LocalUpload :: new ( src, dest, file, opts . attributes ) ) )
419519 }
420520
421521 async fn get_opts ( & self , location : & Path , options : GetOptions ) -> Result < GetResult > {
@@ -434,9 +534,11 @@ impl ObjectStore for LocalFileSystem {
434534 None => 0 ..meta. size ,
435535 } ;
436536
537+ let attributes = get_xattrs ( & path) ?;
538+
437539 Ok ( GetResult {
438540 payload : GetResultPayload :: File ( file, path) ,
439- attributes : Attributes :: default ( ) ,
541+ attributes,
440542 range,
441543 meta,
442544 } )
@@ -835,6 +937,8 @@ struct LocalUpload {
835937 src : Option < PathBuf > ,
836938 /// The next offset to write into the file
837939 offset : u64 ,
940+ /// Attributes to set on the file
941+ attributes : Attributes ,
838942}
839943
840944#[ derive( Debug ) ]
@@ -844,14 +948,15 @@ struct UploadState {
844948}
845949
846950impl LocalUpload {
847- pub ( crate ) fn new ( src : PathBuf , dest : PathBuf , file : File ) -> Self {
951+ pub ( crate ) fn new ( src : PathBuf , dest : PathBuf , file : File , attributes : Attributes ) -> Self {
848952 Self {
849953 state : Arc :: new ( UploadState {
850954 dest,
851955 file : Mutex :: new ( file) ,
852956 } ) ,
853957 src : Some ( src) ,
854958 offset : 0 ,
959+ attributes,
855960 }
856961 }
857962}
@@ -882,9 +987,13 @@ impl MultipartUpload for LocalUpload {
882987 async fn complete ( & mut self ) -> Result < PutResult > {
883988 let src = self . src . take ( ) . ok_or ( Error :: Aborted ) ?;
884989 let s = Arc :: clone ( & self . state ) ;
990+ let attributes = std:: mem:: take ( & mut self . attributes ) ;
885991 maybe_spawn_blocking ( move || {
886992 // Ensure no inflight writes
887993 let file = s. file . lock ( ) ;
994+
995+ set_xattrs ( & src, & attributes) ?;
996+
888997 std:: fs:: rename ( & src, & s. dest )
889998 . map_err ( |source| Error :: UnableToRenameFile { source } ) ?;
890999 let metadata = file. metadata ( ) . map_err ( |e| Error :: Metadata {
@@ -1280,6 +1389,8 @@ mod tests {
12801389 copy_rename_nonexistent_object ( & integration) . await ;
12811390 stream_get ( & integration) . await ;
12821391 put_opts ( & integration, false ) . await ;
1392+ #[ cfg( feature = "xattr" ) ]
1393+ put_get_attributes ( & integration) . await ;
12831394 }
12841395
12851396 #[ test]
@@ -1914,3 +2025,80 @@ mod unix_test {
19142025 spawned. await . unwrap ( ) ;
19152026 }
19162027}
2028+
2029+ #[ cfg( feature = "xattr" ) ]
2030+ #[ cfg( test) ]
2031+ mod xattr_test {
2032+ use tempfile:: TempDir ;
2033+
2034+ use crate :: local:: LocalFileSystem ;
2035+ use crate :: { Attribute , Attributes , ObjectStore , ObjectStoreExt , Path , PutOptions } ;
2036+
2037+ #[ tokio:: test]
2038+ async fn test_put_get_attributes ( ) {
2039+ let root = TempDir :: new ( ) . unwrap ( ) ;
2040+ let integration = LocalFileSystem :: new_with_prefix ( root. path ( ) ) . unwrap ( ) ;
2041+
2042+ let location = Path :: from ( "test_file" ) ;
2043+ let data = "test data" ;
2044+
2045+ let mut attributes = Attributes :: new ( ) ;
2046+ attributes. insert ( Attribute :: ContentType , "text/plain" . into ( ) ) ;
2047+ attributes. insert ( Attribute :: CacheControl , "max-age=3600" . into ( ) ) ;
2048+ attributes. insert ( Attribute :: ContentDisposition , "inline" . into ( ) ) ;
2049+ attributes. insert ( Attribute :: ContentEncoding , "gzip" . into ( ) ) ;
2050+ attributes. insert ( Attribute :: ContentLanguage , "en-US" . into ( ) ) ;
2051+ attributes. insert ( Attribute :: StorageClass , "STANDARD" . into ( ) ) ;
2052+ attributes. insert (
2053+ Attribute :: Metadata ( "custom_key" . into ( ) ) ,
2054+ "custom_value" . into ( ) ,
2055+ ) ;
2056+
2057+ let opts = PutOptions {
2058+ attributes : attributes. clone ( ) ,
2059+ ..Default :: default ( )
2060+ } ;
2061+
2062+ integration
2063+ . put_opts ( & location, data. into ( ) , opts)
2064+ . await
2065+ . unwrap ( ) ;
2066+
2067+ let result = integration. get ( & location) . await . unwrap ( ) ;
2068+ assert_eq ! ( result. attributes, attributes) ;
2069+
2070+ let bytes = result. bytes ( ) . await . unwrap ( ) ;
2071+ assert_eq ! ( bytes. as_ref( ) , data. as_bytes( ) ) ;
2072+ }
2073+
2074+ #[ tokio:: test]
2075+ async fn test_multipart_attributes ( ) {
2076+ let root = TempDir :: new ( ) . unwrap ( ) ;
2077+ let integration = LocalFileSystem :: new_with_prefix ( root. path ( ) ) . unwrap ( ) ;
2078+
2079+ let location = Path :: from ( "multipart_file" ) ;
2080+
2081+ let mut attributes = Attributes :: new ( ) ;
2082+ attributes. insert ( Attribute :: ContentType , "application/octet-stream" . into ( ) ) ;
2083+ attributes. insert ( Attribute :: Metadata ( "part_count" . into ( ) ) , "2" . into ( ) ) ;
2084+
2085+ let opts = crate :: PutMultipartOptions {
2086+ attributes : attributes. clone ( ) ,
2087+ ..Default :: default ( )
2088+ } ;
2089+
2090+ let mut upload = integration
2091+ . put_multipart_opts ( & location, opts)
2092+ . await
2093+ . unwrap ( ) ;
2094+ upload. put_part ( "part1" . into ( ) ) . await . unwrap ( ) ;
2095+ upload. put_part ( "part2" . into ( ) ) . await . unwrap ( ) ;
2096+ upload. complete ( ) . await . unwrap ( ) ;
2097+
2098+ let result = integration. get ( & location) . await . unwrap ( ) ;
2099+ assert_eq ! ( result. attributes, attributes) ;
2100+
2101+ let bytes = result. bytes ( ) . await . unwrap ( ) ;
2102+ assert_eq ! ( bytes. as_ref( ) , b"part1part2" ) ;
2103+ }
2104+ }
0 commit comments