1package safe 2 3import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "sync" 10) 11 12var ( 13 // ErrAlreadyDone is returned when the safe file has already been closed 14 // or committed 15 ErrAlreadyDone = errors.New("safe file was already committed or closed") 16) 17 18// FileWriter is a thread safe writer that does an atomic write to the target file. It allows one 19// writer at a time to acquire a lock, write the file, and atomically replace the contents of the target file. 20type FileWriter struct { 21 tmpFile *os.File 22 path string 23 commitOrClose sync.Once 24} 25 26// CreateFileWriter takes path as an absolute path of the target file and creates a new FileWriter by attempting to create a tempfile 27func CreateFileWriter(path string) (*FileWriter, error) { 28 writer := &FileWriter{path: path} 29 30 directory := filepath.Dir(path) 31 32 tmpFile, err := ioutil.TempFile(directory, filepath.Base(path)) 33 if err != nil { 34 return nil, err 35 } 36 37 writer.tmpFile = tmpFile 38 39 return writer, nil 40} 41 42// Write wraps the temporary file's Write. 43func (fw *FileWriter) Write(p []byte) (n int, err error) { 44 return fw.tmpFile.Write(p) 45} 46 47// Commit will close the temporary file and rename it to the target file name 48// the first call to Commit() will close and delete the temporary file, so 49// subsequently calls to Commit() are gauaranteed to return an error. 50func (fw *FileWriter) Commit() error { 51 err := ErrAlreadyDone 52 53 fw.commitOrClose.Do(func() { 54 if err = fw.tmpFile.Sync(); err != nil { 55 err = fmt.Errorf("syncing temp file: %v", err) 56 return 57 } 58 59 if err = fw.tmpFile.Close(); err != nil { 60 err = fmt.Errorf("closing temp file: %v", err) 61 return 62 } 63 64 if err = fw.rename(); err != nil { 65 err = fmt.Errorf("renaming temp file: %v", err) 66 return 67 } 68 69 if err = fw.syncDir(); err != nil { 70 err = fmt.Errorf("syncing dir: %v", err) 71 return 72 } 73 }) 74 75 return err 76} 77 78// rename renames the temporary file to the target file 79func (fw *FileWriter) rename() error { 80 return os.Rename(fw.tmpFile.Name(), fw.path) 81} 82 83// syncDir will sync the directory 84func (fw *FileWriter) syncDir() error { 85 f, err := os.Open(filepath.Dir(fw.path)) 86 if err != nil { 87 return err 88 } 89 defer f.Close() 90 91 return f.Sync() 92} 93 94// Close will close and remove the temp file artifact if it exists. If the file 95// was already committed, an ErrAlreadyClosed error will be returned and no 96// changes will be made to the filesystem. 97func (fw *FileWriter) Close() error { 98 err := ErrAlreadyDone 99 100 fw.commitOrClose.Do(func() { 101 if err = fw.tmpFile.Close(); err != nil { 102 return 103 } 104 if err = os.Remove(fw.tmpFile.Name()); err != nil && !os.IsNotExist(err) { 105 return 106 } 107 }) 108 109 return err 110} 111