Skip to content

Commit e40cc6a

Browse files
authored
Update atomic fs writer to delay tempfile creation until Write() is called (#3044)
fix: update atomic fs writer to delay tmp creation until Write Signed-off-by: Evan Baker <[email protected]>
1 parent 79c2af5 commit e40cc6a

File tree

2 files changed

+34
-19
lines changed

2 files changed

+34
-19
lines changed

internal/fs/atomic.go

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@ import (
44
"io"
55
"io/fs"
66
"os"
7-
"path"
7+
"path/filepath"
8+
"sync"
89

910
"github.com/pkg/errors"
1011
)
1112

1213
type AtomicWriter struct {
13-
filename string
14-
tempFile *os.File
14+
dir, name string
15+
tempFile *os.File
16+
17+
lock sync.Mutex
1518
}
1619

1720
var _ io.WriteCloser = &AtomicWriter{}
1821

1922
// NewAtomicWriter returns an io.WriteCloser that will write contents to a temp file and move that temp file to the destination filename. If the destination
2023
// filename already exists, this constructor will copy the file to <filename>-old, truncating that file if it already exists.
21-
func NewAtomicWriter(filename string) (*AtomicWriter, error) {
24+
func NewAtomicWriter(f string) (*AtomicWriter, error) {
25+
filename := filepath.Clean(f)
26+
dir, name := filepath.Split(filename)
2227
// if a file already exists, copy it to <filname>-old
2328
exists := true
2429
existingFile, err := os.Open(filename)
@@ -52,29 +57,39 @@ func NewAtomicWriter(filename string) (*AtomicWriter, error) {
5257
}
5358
}
5459

55-
tempFile, err := os.CreateTemp(path.Dir(filename), path.Base(filename)+"*.tmp")
56-
if err != nil {
57-
return nil, errors.Wrap(err, "unable to create temporary file")
58-
}
59-
60-
return &AtomicWriter{filename: filename, tempFile: tempFile}, nil
60+
return &AtomicWriter{dir: dir, name: name}, nil
6161
}
6262

63-
// Close closes the temp file handle and moves the temp file to the final destination
63+
// Close closes the temp file handle and moves the temp file to the final destination.
64+
// Multiple calls to Close will have no effect after the first success.
6465
func (a *AtomicWriter) Close() error {
65-
if err := a.tempFile.Close(); err != nil {
66+
a.lock.Lock()
67+
defer a.lock.Unlock()
68+
if a.tempFile == nil {
69+
return nil
70+
}
71+
if err := a.tempFile.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
6672
return errors.Wrapf(err, "unable to close temp file %s", a.tempFile.Name())
6773
}
68-
69-
if err := os.Rename(a.tempFile.Name(), a.filename); err != nil {
70-
return errors.Wrapf(err, "unable to move temp file %s to destination %s", a.tempFile.Name(), a.filename)
74+
if err := os.Rename(a.tempFile.Name(), filepath.Join(a.dir, a.name)); err != nil {
75+
return errors.Wrapf(err, "unable to move temp file %s to destination %s", a.tempFile.Name(), a.name)
7176
}
72-
77+
a.tempFile = nil
7378
return nil
7479
}
7580

76-
// Write writes the buffer to the temp file. You must call Close() to complete the move from temp file to dest file
81+
// Write writes the buffer to the temp file. You must call Close() to complete the move from temp file to dest file.
82+
// Multiple calls to Write will append to the temp file.
7783
func (a *AtomicWriter) Write(p []byte) (int, error) {
84+
a.lock.Lock()
85+
defer a.lock.Unlock()
86+
if a.tempFile == nil {
87+
tempFile, err := os.CreateTemp(a.dir, a.name+"*.tmp")
88+
if err != nil {
89+
return 0, errors.Wrap(err, "unable to create temporary file")
90+
}
91+
a.tempFile = tempFile
92+
}
7893
bs, err := a.tempFile.Write(p)
7994
return bs, errors.Wrap(err, "unable to write to temp file")
8095
}

internal/fs/atomic_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
func TestAtomicWriterFileExists(t *testing.T) {
13-
file := "testdata/data.txt"
13+
file := "./testdata/data.txt"
1414
w, err := fs.NewAtomicWriter(file)
1515
require.NoError(t, err, "error creating atomic writer")
1616

@@ -37,7 +37,7 @@ func TestAtomicWriterFileExists(t *testing.T) {
3737
}
3838

3939
func TestAtomicWriterNewFile(t *testing.T) {
40-
file := "testdata/newdata.txt"
40+
file := "./testdata/newdata.txt"
4141

4242
// if the file exists before running this test, remove it
4343
err := os.Remove(file)

0 commit comments

Comments
 (0)