@@ -118,13 +118,7 @@ func (o *fsObject) WriteTo(ctx context.Context, dst io.Writer) (n int64, err err
118118}
119119
120120func (o * fsObject ) Put (_ context.Context , data []byte , _ ... PutOption ) error {
121- handle , err := o .getHandle (false )
122- if err != nil {
123- return err
124- }
125- defer handle .Close ()
126-
127- _ , err = io .Copy (handle , bytes .NewReader (data ))
121+ _ , err := o .replaceFrom (bytes .NewReader (data ))
128122
129123 return err
130124}
@@ -155,23 +149,27 @@ func (o *fsObject) StoreFile(ctx context.Context, path string, opts ...PutOption
155149 }
156150 defer r .Close ()
157151
158- handle , err := o . getHandle ( false )
152+ srcInfo , err := r . Stat ( )
159153 if err != nil {
160- return nil , [32 ]byte {}, err
154+ return nil , [32 ]byte {}, fmt .Errorf ("failed to stat file %s: %w" , path , err )
155+ }
156+ if dstInfo , statErr := os .Stat (o .path ); statErr == nil && os .SameFile (srcInfo , dstInfo ) {
157+ return nil , [32 ]byte {}, nil
161158 }
162- defer handle .Close ()
163159
164- n , err := io .Copy (handle , r )
165- if err == nil {
166- logger .L ().Debug (ctx , "Stored file to filesystem" ,
167- zap .String ("object" , o .path ),
168- zap .String ("source" , path ),
169- zap .Int64 ("size_uncompressed" , n ),
170- zap .String ("compression" , "none" ),
171- )
160+ n , err := o .replaceFrom (r )
161+ if err != nil {
162+ return nil , [32 ]byte {}, err
172163 }
173164
174- return nil , [32 ]byte {}, err
165+ logger .L ().Debug (ctx , "Stored file to filesystem" ,
166+ zap .String ("object" , o .path ),
167+ zap .String ("source" , path ),
168+ zap .Int64 ("size_uncompressed" , n ),
169+ zap .String ("compression" , "none" ),
170+ )
171+
172+ return nil , [32 ]byte {}, nil
175173}
176174
177175func (o * fsObject ) storeFileCompressed (ctx context.Context , localPath string , cfg CompressConfig , sink FrameSink ) (* FullFrameTable , [32 ]byte , error ) {
@@ -298,6 +296,60 @@ func (o *fsObject) getHandle(checkExistence bool) (*os.File, error) {
298296 return handle , nil
299297}
300298
299+ func replaceFile (path string , r io.Reader ) (int64 , error ) {
300+ dir := filepath .Dir (path )
301+ if err := os .MkdirAll (dir , 0o755 ); err != nil {
302+ return 0 , err
303+ }
304+
305+ tmp , err := os .CreateTemp (dir , "." + filepath .Base (path )+ ".tmp-*" )
306+ if err != nil {
307+ return 0 , err
308+ }
309+ tmpPath := tmp .Name ()
310+ defer os .Remove (tmpPath )
311+
312+ if err := tmp .Chmod (0o644 ); err != nil {
313+ tmp .Close ()
314+
315+ return 0 , err
316+ }
317+
318+ n , err := io .Copy (tmp , r )
319+ if err != nil {
320+ tmp .Close ()
321+
322+ return n , err
323+ }
324+ if err := tmp .Close (); err != nil {
325+ return n , err
326+ }
327+
328+ if err := os .Rename (tmpPath , path ); err != nil {
329+ return n , err
330+ }
331+
332+ return n , nil
333+ }
334+
335+ func (o * fsObject ) replaceFrom (r io.Reader ) (int64 , error ) {
336+ n , err := replaceFile (o .path , r )
337+ if err != nil {
338+ return n , err
339+ }
340+
341+ return n , o .clearSizeSidecar ()
342+ }
343+
344+ func (o * fsObject ) clearSizeSidecar () error {
345+ err := os .Remove (SizeSidecar (o .path ))
346+ if err == nil || os .IsNotExist (err ) {
347+ return nil
348+ }
349+
350+ return fmt .Errorf ("failed to remove uncompressed-size sidecar for %s: %w" , o .path , err )
351+ }
352+
301353// fsPartUploader implements partUploader for local filesystem.
302354// Embeds memPartUploader for concurrent-safe part collection,
303355// then writes atomically on Complete.
@@ -312,7 +364,9 @@ func (u *fsPartUploader) Complete(_ context.Context) error {
312364 return fmt .Errorf ("failed to create directory: %w" , err )
313365 }
314366
315- return os .WriteFile (u .fullPath , u .Assemble (), 0o644 )
367+ _ , err := replaceFile (u .fullPath , bytes .NewReader (u .Assemble ()))
368+
369+ return err
316370}
317371
318372func (o * fsObject ) OpenRangeReader (ctx context.Context , offsetU int64 , length int64 , frameTable * FrameTable ) (_ RangeReader , _ Source , err error ) {
0 commit comments