package safe import ( "errors" "fmt" "io/ioutil" "os" "path/filepath" "sync" ) var ( // ErrAlreadyDone is returned when the safe file has already been closed // or committed ErrAlreadyDone = errors.New("safe file was already committed or closed") ) // FileWriter is a thread safe writer that does an atomic write to the target file. It allows one // writer at a time to acquire a lock, write the file, and atomically replace the contents of the target file. type FileWriter struct { tmpFile *os.File path string commitOrClose sync.Once } // CreateFileWriter takes path as an absolute path of the target file and creates a new FileWriter by attempting to create a tempfile func CreateFileWriter(path string) (*FileWriter, error) { writer := &FileWriter{path: path} directory := filepath.Dir(path) tmpFile, err := ioutil.TempFile(directory, filepath.Base(path)) if err != nil { return nil, err } writer.tmpFile = tmpFile return writer, nil } // Write wraps the temporary file's Write. func (fw *FileWriter) Write(p []byte) (n int, err error) { return fw.tmpFile.Write(p) } // Commit will close the temporary file and rename it to the target file name // the first call to Commit() will close and delete the temporary file, so // subsequently calls to Commit() are gauaranteed to return an error. func (fw *FileWriter) Commit() error { err := ErrAlreadyDone fw.commitOrClose.Do(func() { if err = fw.tmpFile.Sync(); err != nil { err = fmt.Errorf("syncing temp file: %v", err) return } if err = fw.tmpFile.Close(); err != nil { err = fmt.Errorf("closing temp file: %v", err) return } if err = fw.rename(); err != nil { err = fmt.Errorf("renaming temp file: %v", err) return } if err = fw.syncDir(); err != nil { err = fmt.Errorf("syncing dir: %v", err) return } }) return err } // rename renames the temporary file to the target file func (fw *FileWriter) rename() error { return os.Rename(fw.tmpFile.Name(), fw.path) } // syncDir will sync the directory func (fw *FileWriter) syncDir() error { f, err := os.Open(filepath.Dir(fw.path)) if err != nil { return err } defer f.Close() return f.Sync() } // Close will close and remove the temp file artifact if it exists. If the file // was already committed, an ErrAlreadyClosed error will be returned and no // changes will be made to the filesystem. func (fw *FileWriter) Close() error { err := ErrAlreadyDone fw.commitOrClose.Do(func() { if err = fw.tmpFile.Close(); err != nil { return } if err = os.Remove(fw.tmpFile.Name()); err != nil && !os.IsNotExist(err) { return } }) return err }