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 <rbtr@users.noreply.github.com>
This commit is contained in:
Evan Baker 2024-10-08 13:25:37 -05:00 коммит произвёл GitHub
Родитель 79c2af56a5
Коммит e40cc6afdf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
2 изменённых файлов: 34 добавлений и 19 удалений

Просмотреть файл

@ -4,21 +4,26 @@ import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"sync"
"github.com/pkg/errors"
)
type AtomicWriter struct {
filename string
dir, name string
tempFile *os.File
lock sync.Mutex
}
var _ io.WriteCloser = &AtomicWriter{}
// 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
// filename already exists, this constructor will copy the file to <filename>-old, truncating that file if it already exists.
func NewAtomicWriter(filename string) (*AtomicWriter, error) {
func NewAtomicWriter(f string) (*AtomicWriter, error) {
filename := filepath.Clean(f)
dir, name := filepath.Split(filename)
// if a file already exists, copy it to <filname>-old
exists := true
existingFile, err := os.Open(filename)
@ -52,29 +57,39 @@ func NewAtomicWriter(filename string) (*AtomicWriter, error) {
}
}
tempFile, err := os.CreateTemp(path.Dir(filename), path.Base(filename)+"*.tmp")
if err != nil {
return nil, errors.Wrap(err, "unable to create temporary file")
return &AtomicWriter{dir: dir, name: name}, nil
}
return &AtomicWriter{filename: filename, tempFile: tempFile}, nil
}
// Close closes the temp file handle and moves the temp file to the final destination
// Close closes the temp file handle and moves the temp file to the final destination.
// Multiple calls to Close will have no effect after the first success.
func (a *AtomicWriter) Close() error {
if err := a.tempFile.Close(); err != nil {
a.lock.Lock()
defer a.lock.Unlock()
if a.tempFile == nil {
return nil
}
if err := a.tempFile.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
return errors.Wrapf(err, "unable to close temp file %s", a.tempFile.Name())
}
if err := os.Rename(a.tempFile.Name(), a.filename); err != nil {
return errors.Wrapf(err, "unable to move temp file %s to destination %s", a.tempFile.Name(), a.filename)
if err := os.Rename(a.tempFile.Name(), filepath.Join(a.dir, a.name)); err != nil {
return errors.Wrapf(err, "unable to move temp file %s to destination %s", a.tempFile.Name(), a.name)
}
a.tempFile = nil
return nil
}
// Write writes the buffer to the temp file. You must call Close() to complete the move from temp file to dest file
// Write writes the buffer to the temp file. You must call Close() to complete the move from temp file to dest file.
// Multiple calls to Write will append to the temp file.
func (a *AtomicWriter) Write(p []byte) (int, error) {
a.lock.Lock()
defer a.lock.Unlock()
if a.tempFile == nil {
tempFile, err := os.CreateTemp(a.dir, a.name+"*.tmp")
if err != nil {
return 0, errors.Wrap(err, "unable to create temporary file")
}
a.tempFile = tempFile
}
bs, err := a.tempFile.Write(p)
return bs, errors.Wrap(err, "unable to write to temp file")
}

Просмотреть файл

@ -10,7 +10,7 @@ import (
)
func TestAtomicWriterFileExists(t *testing.T) {
file := "testdata/data.txt"
file := "./testdata/data.txt"
w, err := fs.NewAtomicWriter(file)
require.NoError(t, err, "error creating atomic writer")
@ -37,7 +37,7 @@ func TestAtomicWriterFileExists(t *testing.T) {
}
func TestAtomicWriterNewFile(t *testing.T) {
file := "testdata/newdata.txt"
file := "./testdata/newdata.txt"
// if the file exists before running this test, remove it
err := os.Remove(file)