Skip to content

Commit 945f872

Browse files
authored
Merge pull request #1554 from n1hility/update-atomicwriter
Improve AtomicFileWriter's support for Win and Mac
2 parents 8d58bbb + 21ec71c commit 945f872

File tree

5 files changed

+179
-53
lines changed

5 files changed

+179
-53
lines changed

pkg/ioutils/fswriters.go

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2236
var 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.
4057
func 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

93111
type 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

102122
func (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
}

pkg/ioutils/fswriters_linux.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ import (
66
"golang.org/x/sys/unix"
77
)
88

9-
func fdatasync(f *os.File) error {
9+
func dataOrFullSync(f *os.File) error {
1010
return unix.Fdatasync(int(f.Fd()))
1111
}
12+
13+
func (w *atomicFileWriter) postDataWrittenSync() error {
14+
if w.noSync {
15+
return nil
16+
}
17+
return unix.Fdatasync(int(w.f.Fd()))
18+
}
19+
20+
func (w *atomicFileWriter) preRenameSync() error {
21+
// On Linux data can be reliably flushed to media without metadata, so defer
22+
return nil
23+
}

pkg/ioutils/fswriters_other.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//go:build !linux
2+
// +build !linux
3+
4+
package ioutils
5+
6+
import (
7+
"os"
8+
)
9+
10+
func dataOrFullSync(f *os.File) error {
11+
return f.Sync()
12+
}
13+
14+
func (w *atomicFileWriter) postDataWrittenSync() error {
15+
// many platforms (Mac, Windows) require a full sync to reliably flush to media
16+
return nil
17+
}
18+
19+
func (w *atomicFileWriter) preRenameSync() error {
20+
if w.noSync {
21+
return nil
22+
}
23+
24+
// fsync() on Non-linux Unix, FlushFileBuffers (Windows), F_FULLFSYNC (Mac)
25+
return w.f.Sync()
26+
}

pkg/ioutils/fswriters_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,56 @@ func TestAtomicWriteToFile(t *testing.T) {
4545
}
4646
}
4747

48+
func TestAtomicCommitAndRollbackFile(t *testing.T) {
49+
tmpDir := t.TempDir()
50+
path := filepath.Join(tmpDir, "foo")
51+
52+
oldData := "olddata"
53+
newData := "newdata"
54+
55+
check := func(n int, initData string, writeData string, expected string, explicit bool, commit bool) {
56+
if err := os.WriteFile(path, []byte(initData), 0644); err != nil {
57+
t.Fatalf("Failed creating initial file: %v", err)
58+
}
59+
60+
opts := &AtomicFileWriterOptions{ExplicitCommit: explicit}
61+
w, err := NewAtomicFileWriterWithOpts(filepath.Join(tmpDir, "foo"), 0644, opts)
62+
63+
if err != nil {
64+
t.Fatalf("(%d) Failed creating writer: %v", n, err)
65+
}
66+
67+
if _, err := w.Write([]byte(writeData)); err != nil {
68+
t.Fatalf("(%d) Failed writing content: %v", n, err)
69+
}
70+
71+
if commit {
72+
if err := w.Commit(); err != nil {
73+
t.Fatalf("(%d) Failed committing writer: %v", n, err)
74+
}
75+
}
76+
77+
if err := w.Close(); err != nil {
78+
t.Fatalf("(%d) Failed closing writer: %v", n, err)
79+
}
80+
81+
actual, err := os.ReadFile(filepath.Join(tmpDir, "foo"))
82+
if err != nil {
83+
t.Fatalf("(%d) Error reading from file: %v", n, err)
84+
}
85+
86+
// Verify write never happened since no call to commit
87+
if !bytes.Equal(actual, []byte(expected)) {
88+
t.Fatalf("(%d) Data mismatch, expected %q, got %q", n, expected, actual)
89+
}
90+
}
91+
92+
check(1, oldData, newData, oldData, true, false)
93+
check(2, oldData, newData, newData, true, true)
94+
check(3, oldData, newData, newData, false, false)
95+
check(4, oldData, newData, newData, false, true)
96+
}
97+
4898
func TestAtomicWriteSetCommit(t *testing.T) {
4999
tmpDir := t.TempDir()
50100

pkg/ioutils/fswriters_unsupported.go

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)