github.com/richardwilkes/toolbox@v1.121.0/xio/fs/safe/file.go (about) 1 // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 // 3 // This Source Code Form is subject to the terms of the Mozilla Public 4 // License, version 2.0. If a copy of the MPL was not distributed with 5 // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 // 7 // This Source Code Form is "Incompatible With Secondary Licenses", as 8 // defined by the Mozilla Public License, version 2.0. 9 10 // Package safe provides safe, atomic saving of files. 11 package safe 12 13 import ( 14 "os" 15 "path/filepath" 16 17 "github.com/richardwilkes/toolbox/xio/fs/internal" 18 ) 19 20 // File provides safe, atomic saving of files. Instead of truncating and overwriting the destination file, it creates a 21 // temporary file in the same directory, writes to it, and then renames the temporary file to the original name when 22 // Commit() is called. If Close() is called without calling Commit(), or the Commit() fails, then the original file is 23 // left untouched. 24 type File struct { 25 *os.File 26 originalName string 27 committed bool 28 closed bool 29 } 30 31 // Create creates a temporary file in the same directory as filename, which will be renamed to the given filename when 32 // calling Commit. 33 func Create(filename string) (*File, error) { 34 return CreateWithMode(filename, 0o644) 35 } 36 37 // CreateWithMode creates a temporary file in the same directory as filename, which will be renamed to the given 38 // filename when calling Commit. 39 func CreateWithMode(filename string, mode os.FileMode) (*File, error) { 40 filename = filepath.Clean(filename) 41 if filename == "" || filename[len(filename)-1] == filepath.Separator { 42 return nil, os.ErrInvalid 43 } 44 f, err := internal.CreateTemp(filepath.Dir(filename), "safe", mode) 45 if err != nil { 46 return nil, err 47 } 48 return &File{ 49 File: f, 50 originalName: filename, 51 }, nil 52 } 53 54 // OriginalName returns the original filename passed into Create(). 55 func (f *File) OriginalName() string { 56 return f.originalName 57 } 58 59 // Commit the data into the original file and remove the temporary file from disk. Close() may still be called, but will 60 // do nothing. 61 func (f *File) Commit() error { 62 if f.committed { 63 return nil 64 } 65 if f.closed { 66 return os.ErrInvalid 67 } 68 f.committed = true 69 f.closed = true 70 var err error 71 name := f.Name() 72 defer func() { 73 if err != nil { 74 _ = os.Remove(name) //nolint:errcheck // no need to report this error, too 75 } 76 }() 77 if err = f.File.Close(); err != nil { 78 return err 79 } 80 err = os.Rename(name, f.originalName) 81 return err 82 } 83 84 // Close the temporary file and remove it, if it hasn't already been committed. If it has been committed, nothing 85 // happens. 86 func (f *File) Close() error { 87 if f.committed { 88 return nil 89 } 90 if f.closed { 91 return os.ErrInvalid 92 } 93 f.closed = true 94 err := f.File.Close() 95 if removeErr := os.Remove(f.Name()); removeErr != nil && err == nil { 96 err = removeErr 97 } 98 return err 99 }