github.com/safing/portbase@v0.19.5/utils/atomic.go (about) 1 package utils 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/fs" 8 "os" 9 10 "github.com/safing/portbase/utils/renameio" 11 ) 12 13 // AtomicFileOptions holds additional options for manipulating 14 // the behavior of CreateAtomic and friends. 15 type AtomicFileOptions struct { 16 // Mode is the file mode for the new file. If 17 // 0, the file mode will be set to 0600. 18 Mode os.FileMode 19 20 // TempDir is the path to the temp-directory 21 // that should be used. If empty, it defaults 22 // to the system temp. 23 TempDir string 24 } 25 26 // CreateAtomic creates or overwrites a file at dest atomically using 27 // data from r. Atomic means that even in case of a power outage, 28 // dest will never be a zero-length file. It will always either contain 29 // the previous data (or not exist) or the new data but never anything 30 // in between. 31 func CreateAtomic(dest string, r io.Reader, opts *AtomicFileOptions) error { 32 if opts == nil { 33 opts = &AtomicFileOptions{} 34 } 35 36 tmpFile, err := renameio.TempFile(opts.TempDir, dest) 37 if err != nil { 38 return fmt.Errorf("failed to create temp file: %w", err) 39 } 40 defer tmpFile.Cleanup() //nolint:errcheck 41 42 if opts.Mode != 0 { 43 if err := tmpFile.Chmod(opts.Mode); err != nil { 44 return fmt.Errorf("failed to update mode bits of temp file: %w", err) 45 } 46 } 47 48 if _, err := io.Copy(tmpFile, r); err != nil { 49 return fmt.Errorf("failed to copy source file: %w", err) 50 } 51 52 if err := tmpFile.CloseAtomicallyReplace(); err != nil { 53 return fmt.Errorf("failed to rename temp file to %q", dest) 54 } 55 56 return nil 57 } 58 59 // CopyFileAtomic is like CreateAtomic but copies content from 60 // src to dest. If opts.Mode is 0 CopyFileAtomic tries to set 61 // the file mode of src to dest. 62 func CopyFileAtomic(dest string, src string, opts *AtomicFileOptions) error { 63 if opts == nil { 64 opts = &AtomicFileOptions{} 65 } 66 67 if opts.Mode == 0 { 68 stat, err := os.Stat(src) 69 if err != nil { 70 return err 71 } 72 opts.Mode = stat.Mode() 73 } 74 75 f, err := os.Open(src) 76 if err != nil { 77 return err 78 } 79 defer func() { 80 _ = f.Close() 81 }() 82 83 return CreateAtomic(dest, f, opts) 84 } 85 86 // ReplaceFileAtomic replaces the file at dest with the content from src. 87 // If dest exists it's file mode copied and used for the replacement. If 88 // not, dest will get the same file mode as src. See CopyFileAtomic and 89 // CreateAtomic for more information. 90 func ReplaceFileAtomic(dest string, src string, opts *AtomicFileOptions) error { 91 if opts == nil { 92 opts = &AtomicFileOptions{} 93 } 94 95 if opts.Mode == 0 { 96 stat, err := os.Stat(dest) 97 if err == nil { 98 opts.Mode = stat.Mode() 99 } else if !errors.Is(err, fs.ErrNotExist) { 100 return err 101 } 102 } 103 104 return CopyFileAtomic(dest, src, opts) 105 }