1616// under the License.
1717
1818//! An object store implementation for a local filesystem
19+ #[ cfg( feature = "xattr" ) ]
20+ use std:: borrow:: Cow ;
1921use std:: fs:: { File , Metadata , OpenOptions , metadata, symlink_metadata} ;
2022use std:: io:: { ErrorKind , Read , Seek , SeekFrom , Write } ;
2123use std:: ops:: Range ;
@@ -33,6 +35,8 @@ use parking_lot::Mutex;
3335use url:: Url ;
3436use walkdir:: { DirEntry , WalkDir } ;
3537
38+ #[ cfg( feature = "xattr" ) ]
39+ use crate :: Attribute ;
3640use crate :: {
3741 Attributes , GetOptions , GetResult , GetResultPayload , ListResult , MultipartUpload , ObjectMeta ,
3842 ObjectStore , PutMode , PutMultipartOptions , PutOptions , PutPayload , PutResult , Result ,
@@ -112,6 +116,20 @@ pub(crate) enum Error {
112116
113117 #[ error( "Upload aborted" ) ]
114118 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+ } ,
115133}
116134
117135impl From < Error > for super :: Error {
@@ -321,6 +339,98 @@ fn is_valid_file_path(path: &Path) -> bool {
321339 }
322340}
323341
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+ for ( attr, value) in attributes {
346+ let name: Cow < ' static , str > = match attr {
347+ Attribute :: CacheControl => Cow :: Borrowed ( "user.object_store.cache_control" ) ,
348+ Attribute :: ContentDisposition => Cow :: Borrowed ( "user.object_store.content_disposition" ) ,
349+ Attribute :: ContentEncoding => Cow :: Borrowed ( "user.object_store.content_encoding" ) ,
350+ Attribute :: ContentLanguage => Cow :: Borrowed ( "user.object_store.content_language" ) ,
351+ Attribute :: ContentType => Cow :: Borrowed ( "user.object_store.content_type" ) ,
352+ Attribute :: StorageClass => Cow :: Borrowed ( "user.object_store.storage_class" ) ,
353+ Attribute :: Metadata ( key) => Cow :: Owned ( format ! ( "user.object_store.{key}" ) ) ,
354+ } ;
355+ xattr:: set ( path, name. as_ref ( ) , value. as_ref ( ) . as_bytes ( ) ) . map_err ( |source| {
356+ Error :: UnableToSetXattr {
357+ source,
358+ path : path. into ( ) ,
359+ }
360+ } ) ?;
361+ }
362+ Ok ( ( ) )
363+ }
364+
365+ /// Reads extended attributes from a file and returns an Attributes collection
366+ #[ cfg( feature = "xattr" ) ]
367+ fn get_xattrs ( path : & std:: path:: Path ) -> Result < Attributes > {
368+ let mut attributes = Attributes :: new ( ) ;
369+
370+ let list = match xattr:: list ( path) {
371+ Ok ( list) => list,
372+ Err ( source) => {
373+ return Err ( Error :: UnableToReadXattr {
374+ source,
375+ path : path. into ( ) ,
376+ }
377+ . into ( ) ) ;
378+ }
379+ } ;
380+
381+ for name in list {
382+ let name_str = match name. to_str ( ) {
383+ Some ( s) if s. starts_with ( "user.object_store." ) => s,
384+ _ => continue ,
385+ } ;
386+
387+ let value = match xattr:: get ( path, & name) {
388+ Ok ( Some ( v) ) => match String :: from_utf8 ( v) {
389+ Ok ( s) => s,
390+ Err ( _) => continue ,
391+ } ,
392+ Ok ( None ) => continue ,
393+ Err ( source) => {
394+ return Err ( Error :: UnableToReadXattr {
395+ source,
396+ path : path. into ( ) ,
397+ }
398+ . into ( ) ) ;
399+ }
400+ } ;
401+
402+ let key = & name_str[ "user.object_store." . len ( ) ..] ;
403+ let attr = match key {
404+ "cache_control" => Attribute :: CacheControl ,
405+ "content_disposition" => Attribute :: ContentDisposition ,
406+ "content_encoding" => Attribute :: ContentEncoding ,
407+ "content_language" => Attribute :: ContentLanguage ,
408+ "content_type" => Attribute :: ContentType ,
409+ "storage_class" => Attribute :: StorageClass ,
410+ key => Attribute :: Metadata ( key. to_string ( ) . into ( ) ) ,
411+ } ;
412+ attributes. insert ( attr, value. into ( ) ) ;
413+ }
414+ Ok ( attributes)
415+ }
416+
417+ /// Returns an error if attributes are non-empty when xattr feature is disabled.
418+ #[ cfg( not( feature = "xattr" ) ) ]
419+ fn set_xattrs ( _path : & std:: path:: Path , attributes : & Attributes ) -> Result < ( ) > {
420+ if !attributes. is_empty ( ) {
421+ return Err ( super :: Error :: NotSupported {
422+ source : "Setting extended attributes requires the 'xattr' feature" . into ( ) ,
423+ } ) ;
424+ }
425+ Ok ( ( ) )
426+ }
427+
428+ /// No-op when xattr feature is disabled: returns empty attributes.
429+ #[ cfg( not( feature = "xattr" ) ) ]
430+ fn get_xattrs ( _path : & std:: path:: Path ) -> Result < Attributes > {
431+ Ok ( Attributes :: new ( ) )
432+ }
433+
324434#[ async_trait]
325435impl ObjectStore for LocalFileSystem {
326436 async fn put_opts (
@@ -336,13 +446,6 @@ impl ObjectStore for LocalFileSystem {
336446 } ) ;
337447 }
338448
339- if !opts. attributes . is_empty ( ) {
340- return Err ( crate :: Error :: NotImplemented {
341- operation : "`put_opts` with `opts.attributes` specified" . into ( ) ,
342- implementer : self . to_string ( ) ,
343- } ) ;
344- }
345-
346449 let path = self . path_to_filesystem ( location) ?;
347450 maybe_spawn_blocking ( move || {
348451 let ( mut file, staging_path) = new_staged_upload ( & path) ?;
@@ -355,6 +458,11 @@ impl ObjectStore for LocalFileSystem {
355458 path : path. to_string_lossy ( ) . to_string ( ) ,
356459 } ) ?;
357460 e_tag = Some ( get_etag ( & metadata) ) ;
461+
462+ if !opts. attributes . is_empty ( ) {
463+ set_xattrs ( & staging_path, & opts. attributes ) ?;
464+ }
465+
358466 match opts. mode {
359467 PutMode :: Overwrite => {
360468 // For some fuse types of file systems, the file must be closed first
@@ -402,16 +510,9 @@ impl ObjectStore for LocalFileSystem {
402510 location : & Path ,
403511 opts : PutMultipartOptions ,
404512 ) -> Result < Box < dyn MultipartUpload > > {
405- if !opts. attributes . is_empty ( ) {
406- return Err ( crate :: Error :: NotImplemented {
407- operation : "`put_multipart_opts` with `opts.attributes` specified" . into ( ) ,
408- implementer : self . to_string ( ) ,
409- } ) ;
410- }
411-
412513 let dest = self . path_to_filesystem ( location) ?;
413514 let ( file, src) = new_staged_upload ( & dest) ?;
414- Ok ( Box :: new ( LocalUpload :: new ( src, dest, file) ) )
515+ Ok ( Box :: new ( LocalUpload :: new ( src, dest, file, opts . attributes ) ) )
415516 }
416517
417518 async fn get_opts ( & self , location : & Path , options : GetOptions ) -> Result < GetResult > {
@@ -429,9 +530,11 @@ impl ObjectStore for LocalFileSystem {
429530 None => 0 ..meta. size ,
430531 } ;
431532
533+ let attributes = get_xattrs ( & path) ?;
534+
432535 Ok ( GetResult {
433536 payload : GetResultPayload :: File ( file, path) ,
434- attributes : Attributes :: default ( ) ,
537+ attributes,
435538 range,
436539 meta,
437540 } )
@@ -829,6 +932,8 @@ struct LocalUpload {
829932 src : Option < PathBuf > ,
830933 /// The next offset to write into the file
831934 offset : u64 ,
935+ /// Attributes to set on the file
936+ attributes : Attributes ,
832937}
833938
834939#[ derive( Debug ) ]
@@ -838,14 +943,15 @@ struct UploadState {
838943}
839944
840945impl LocalUpload {
841- pub ( crate ) fn new ( src : PathBuf , dest : PathBuf , file : File ) -> Self {
946+ pub ( crate ) fn new ( src : PathBuf , dest : PathBuf , file : File , attributes : Attributes ) -> Self {
842947 Self {
843948 state : Arc :: new ( UploadState {
844949 dest,
845950 file : Mutex :: new ( file) ,
846951 } ) ,
847952 src : Some ( src) ,
848953 offset : 0 ,
954+ attributes,
849955 }
850956 }
851957}
@@ -876,9 +982,15 @@ impl MultipartUpload for LocalUpload {
876982 async fn complete ( & mut self ) -> Result < PutResult > {
877983 let src = self . src . take ( ) . ok_or ( Error :: Aborted ) ?;
878984 let s = Arc :: clone ( & self . state ) ;
985+ let attributes = std:: mem:: take ( & mut self . attributes ) ;
879986 maybe_spawn_blocking ( move || {
880987 // Ensure no inflight writes
881988 let file = s. file . lock ( ) ;
989+
990+ if !attributes. is_empty ( ) {
991+ set_xattrs ( & src, & attributes) ?;
992+ }
993+
882994 std:: fs:: rename ( & src, & s. dest )
883995 . map_err ( |source| Error :: UnableToRenameFile { source } ) ?;
884996 let metadata = file. metadata ( ) . map_err ( |e| Error :: Metadata {
@@ -1177,6 +1289,8 @@ mod tests {
11771289 copy_rename_nonexistent_object ( & integration) . await ;
11781290 stream_get ( & integration) . await ;
11791291 put_opts ( & integration, false ) . await ;
1292+ #[ cfg( feature = "xattr" ) ]
1293+ put_get_attributes ( & integration) . await ;
11801294 }
11811295
11821296 #[ test]
@@ -1811,3 +1925,80 @@ mod unix_test {
18111925 spawned. await . unwrap ( ) ;
18121926 }
18131927}
1928+
1929+ #[ cfg( feature = "xattr" ) ]
1930+ #[ cfg( test) ]
1931+ mod xattr_test {
1932+ use tempfile:: TempDir ;
1933+
1934+ use crate :: local:: LocalFileSystem ;
1935+ use crate :: { Attribute , Attributes , ObjectStore , ObjectStoreExt , Path , PutOptions } ;
1936+
1937+ #[ tokio:: test]
1938+ async fn test_put_get_attributes ( ) {
1939+ let root = TempDir :: new ( ) . unwrap ( ) ;
1940+ let integration = LocalFileSystem :: new_with_prefix ( root. path ( ) ) . unwrap ( ) ;
1941+
1942+ let location = Path :: from ( "test_file" ) ;
1943+ let data = "test data" ;
1944+
1945+ let mut attributes = Attributes :: new ( ) ;
1946+ attributes. insert ( Attribute :: ContentType , "text/plain" . into ( ) ) ;
1947+ attributes. insert ( Attribute :: CacheControl , "max-age=3600" . into ( ) ) ;
1948+ attributes. insert ( Attribute :: ContentDisposition , "inline" . into ( ) ) ;
1949+ attributes. insert ( Attribute :: ContentEncoding , "gzip" . into ( ) ) ;
1950+ attributes. insert ( Attribute :: ContentLanguage , "en-US" . into ( ) ) ;
1951+ attributes. insert ( Attribute :: StorageClass , "STANDARD" . into ( ) ) ;
1952+ attributes. insert (
1953+ Attribute :: Metadata ( "custom_key" . into ( ) ) ,
1954+ "custom_value" . into ( ) ,
1955+ ) ;
1956+
1957+ let opts = PutOptions {
1958+ attributes : attributes. clone ( ) ,
1959+ ..Default :: default ( )
1960+ } ;
1961+
1962+ integration
1963+ . put_opts ( & location, data. into ( ) , opts)
1964+ . await
1965+ . unwrap ( ) ;
1966+
1967+ let result = integration. get ( & location) . await . unwrap ( ) ;
1968+ assert_eq ! ( result. attributes, attributes) ;
1969+
1970+ let bytes = result. bytes ( ) . await . unwrap ( ) ;
1971+ assert_eq ! ( bytes. as_ref( ) , data. as_bytes( ) ) ;
1972+ }
1973+
1974+ #[ tokio:: test]
1975+ async fn test_multipart_attributes ( ) {
1976+ let root = TempDir :: new ( ) . unwrap ( ) ;
1977+ let integration = LocalFileSystem :: new_with_prefix ( root. path ( ) ) . unwrap ( ) ;
1978+
1979+ let location = Path :: from ( "multipart_file" ) ;
1980+
1981+ let mut attributes = Attributes :: new ( ) ;
1982+ attributes. insert ( Attribute :: ContentType , "application/octet-stream" . into ( ) ) ;
1983+ attributes. insert ( Attribute :: Metadata ( "part_count" . into ( ) ) , "2" . into ( ) ) ;
1984+
1985+ let opts = crate :: PutMultipartOptions {
1986+ attributes : attributes. clone ( ) ,
1987+ ..Default :: default ( )
1988+ } ;
1989+
1990+ let mut upload = integration
1991+ . put_multipart_opts ( & location, opts)
1992+ . await
1993+ . unwrap ( ) ;
1994+ upload. put_part ( "part1" . into ( ) ) . await . unwrap ( ) ;
1995+ upload. put_part ( "part2" . into ( ) ) . await . unwrap ( ) ;
1996+ upload. complete ( ) . await . unwrap ( ) ;
1997+
1998+ let result = integration. get ( & location) . await . unwrap ( ) ;
1999+ assert_eq ! ( result. attributes, attributes) ;
2000+
2001+ let bytes = result. bytes ( ) . await . unwrap ( ) ;
2002+ assert_eq ! ( bytes. as_ref( ) , b"part1part2" ) ;
2003+ }
2004+ }
0 commit comments