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