@@ -17,6 +17,20 @@ type AtomicFileWriterOptions struct {
1717 // On successful return from Close() this is set to the mtime of the
1818 // newly written file.
1919 ModTime time.Time
20+ // Specifies whether Commit() must be explicitly called to write state
21+ // to the destination. This allows an application to preserve the original
22+ // file when an error occurs during processing (and not just during write)
23+ // The default is false, which will auto-commit on Close
24+ ExplicitCommit bool
25+ }
26+
27+ type CommittableWriter interface {
28+ io.WriteCloser
29+
30+ // Commit closes the temporary file associated with this writer, and
31+ // provided no errors (during commit or previously during write operations),
32+ // will publish the completed file under the intended destination.
33+ Commit () error
2034}
2135
2236var defaultWriterOptions = AtomicFileWriterOptions {}
@@ -27,16 +41,19 @@ func SetDefaultOptions(opts AtomicFileWriterOptions) {
2741 defaultWriterOptions = opts
2842}
2943
30- // NewAtomicFileWriterWithOpts returns WriteCloser so that writing to it writes to a
31- // temporary file and closing it atomically changes the temporary file to
32- // destination path. Writing and closing concurrently is not allowed.
33- func NewAtomicFileWriterWithOpts (filename string , perm os.FileMode , opts * AtomicFileWriterOptions ) (io.WriteCloser , error ) {
44+ // NewAtomicFileWriterWithOpts returns a CommittableWriter so that writing to it
45+ // writes to a temporary file, which can later be committed to a destination path,
46+ // either by Closing in the case of auto-commit, or manually calling commit if the
47+ // ExplicitCommit option is enabled. Writing and closing concurrently is not
48+ // allowed.
49+ func NewAtomicFileWriterWithOpts (filename string , perm os.FileMode , opts * AtomicFileWriterOptions ) (CommittableWriter , error ) {
3450 return newAtomicFileWriter (filename , perm , opts )
3551}
3652
37- // newAtomicFileWriter returns WriteCloser so that writing to it writes to a
38- // temporary file and closing it atomically changes the temporary file to
39- // destination path. Writing and closing concurrently is not allowed.
53+ // newAtomicFileWriter returns a CommittableWriter so that writing to it writes to
54+ // a temporary file, which can later be committed to a destination path, either by
55+ // Closing in the case of auto-commit, or manually calling commit if the
56+ // ExplicitCommit option is enabled. Writing and closing concurrently is not allowed.
4057func newAtomicFileWriter (filename string , perm os.FileMode , opts * AtomicFileWriterOptions ) (* atomicFileWriter , error ) {
4158 f , err := os .CreateTemp (filepath .Dir (filename ), ".tmp-" + filepath .Base (filename ))
4259 if err != nil {
@@ -50,17 +67,18 @@ func newAtomicFileWriter(filename string, perm os.FileMode, opts *AtomicFileWrit
5067 return nil , err
5168 }
5269 return & atomicFileWriter {
53- f : f ,
54- fn : abspath ,
55- perm : perm ,
56- noSync : opts .NoSync ,
70+ f : f ,
71+ fn : abspath ,
72+ perm : perm ,
73+ noSync : opts .NoSync ,
74+ explicitCommit : opts .ExplicitCommit ,
5775 }, nil
5876}
5977
60- // NewAtomicFileWriter returns WriteCloser so that writing to it writes to a
61- // temporary file and closing it atomically changes the temporary file to
62- // destination path. Writing and closing concurrently is not allowed.
63- func NewAtomicFileWriter (filename string , perm os.FileMode ) (io. WriteCloser , error ) {
78+ // NewAtomicFileWriterWithOpts returns a CommittableWriter, with auto-commit enabled.
79+ // Writing to it writes to a temporary file and closing it atomically changes the
80+ // temporary file to destination path. Writing and closing concurrently is not allowed.
81+ func NewAtomicFileWriter (filename string , perm os.FileMode ) (CommittableWriter , error ) {
6482 return NewAtomicFileWriterWithOpts (filename , perm , nil )
6583}
6684
@@ -91,12 +109,14 @@ func AtomicWriteFile(filename string, data []byte, perm os.FileMode) error {
91109}
92110
93111type atomicFileWriter struct {
94- f * os.File
95- fn string
96- writeErr error
97- perm os.FileMode
98- noSync bool
99- modTime time.Time
112+ f * os.File
113+ fn string
114+ writeErr error
115+ perm os.FileMode
116+ noSync bool
117+ modTime time.Time
118+ closed bool
119+ explicitCommit bool
100120}
101121
102122func (w * atomicFileWriter ) Write (dt []byte ) (int , error ) {
@@ -107,43 +127,73 @@ func (w *atomicFileWriter) Write(dt []byte) (int, error) {
107127 return n , err
108128}
109129
110- func (w * atomicFileWriter ) Close () (retErr error ) {
130+ func (w * atomicFileWriter ) closeTempFile () error {
131+ if w .closed {
132+ return nil
133+ }
134+
135+ w .closed = true
136+ return w .f .Close ()
137+ }
138+
139+ func (w * atomicFileWriter ) Close () error {
140+ return w .complete (! w .explicitCommit )
141+ }
142+
143+ func (w * atomicFileWriter ) Commit () error {
144+ return w .complete (true )
145+ }
146+
147+ func (w * atomicFileWriter ) complete (commit bool ) (retErr error ) {
148+ if w == nil || w .closed {
149+ return nil
150+ }
151+
111152 defer func () {
153+ w .closeTempFile ()
112154 if retErr != nil || w .writeErr != nil {
113155 os .Remove (w .f .Name ())
114156 }
115157 }()
116- if ! w .noSync {
117- if err := fdatasync (w .f ); err != nil {
118- w .f .Close ()
119- return err
120- }
158+
159+ if commit {
160+ return w .commitState ()
121161 }
122162
123- // fstat before closing the fd
124- info , statErr := w .f .Stat ()
125- if statErr == nil {
126- w .modTime = info .ModTime ()
163+ return nil
164+ }
165+
166+ func (w * atomicFileWriter ) commitState () error {
167+ // Perform a data only sync (fdatasync()) if supported
168+ if err := w .postDataWrittenSync (); err != nil {
169+ return err
127170 }
128- // We delay error reporting until after the real call to close()
129- // to match the traditional linux close() behaviour that an fd
130- // is invalid (closed) even if close returns failure. While
131- // weird, this allows a well defined way to not leak open fds.
132171
133- if err := w .f .Close (); err != nil {
172+ // Capture fstat before closing the fd
173+ info , err := w .f .Stat ()
174+ if err != nil {
134175 return err
135176 }
177+ w .modTime = info .ModTime ()
136178
137- if statErr != nil {
138- return statErr
179+ if err := w .f .Chmod (w .perm ); err != nil {
180+ return err
181+ }
182+
183+ // Perform full sync on platforms that need it
184+ if err := w .preRenameSync (); err != nil {
185+ return err
139186 }
140187
141- if err := os .Chmod (w .f .Name (), w .perm ); err != nil {
188+ // Some platforms require closing before rename (Windows)
189+ if err := w .closeTempFile (); err != nil {
142190 return err
143191 }
192+
144193 if w .writeErr == nil {
145194 return os .Rename (w .f .Name (), w .fn )
146195 }
196+
147197 return nil
148198}
149199
@@ -195,7 +245,7 @@ func (w syncFileCloser) Close() error {
195245 if ! defaultWriterOptions .NoSync {
196246 return w .File .Close ()
197247 }
198- err := fdatasync (w .File )
248+ err := dataOrFullSync (w .File )
199249 if err1 := w .File .Close (); err == nil {
200250 err = err1
201251 }
0 commit comments