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:
Родитель
79c2af56a5
Коммит
e40cc6afdf
|
@ -4,21 +4,26 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AtomicWriter struct {
|
type AtomicWriter struct {
|
||||||
filename string
|
dir, name string
|
||||||
tempFile *os.File
|
tempFile *os.File
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ io.WriteCloser = &AtomicWriter{}
|
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
|
// 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.
|
// 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
|
// if a file already exists, copy it to <filname>-old
|
||||||
exists := true
|
exists := true
|
||||||
existingFile, err := os.Open(filename)
|
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")
|
return &AtomicWriter{dir: dir, name: name}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "unable to create temporary file")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
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())
|
return errors.Wrapf(err, "unable to close temp file %s", a.tempFile.Name())
|
||||||
}
|
}
|
||||||
|
if err := os.Rename(a.tempFile.Name(), filepath.Join(a.dir, a.name)); err != nil {
|
||||||
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.name)
|
||||||
return errors.Wrapf(err, "unable to move temp file %s to destination %s", a.tempFile.Name(), a.filename)
|
|
||||||
}
|
}
|
||||||
|
a.tempFile = nil
|
||||||
return 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) {
|
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)
|
bs, err := a.tempFile.Write(p)
|
||||||
return bs, errors.Wrap(err, "unable to write to temp file")
|
return bs, errors.Wrap(err, "unable to write to temp file")
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAtomicWriterFileExists(t *testing.T) {
|
func TestAtomicWriterFileExists(t *testing.T) {
|
||||||
file := "testdata/data.txt"
|
file := "./testdata/data.txt"
|
||||||
w, err := fs.NewAtomicWriter(file)
|
w, err := fs.NewAtomicWriter(file)
|
||||||
require.NoError(t, err, "error creating atomic writer")
|
require.NoError(t, err, "error creating atomic writer")
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ func TestAtomicWriterFileExists(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAtomicWriterNewFile(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
|
// if the file exists before running this test, remove it
|
||||||
err := os.Remove(file)
|
err := os.Remove(file)
|
||||||
|
|
Загрузка…
Ссылка в новой задаче