all: sync with master; upd chlog
This commit is contained in:
52
internal/aghrenameio/renameio.go
Normal file
52
internal/aghrenameio/renameio.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Package aghrenameio is a wrapper around package github.com/google/renameio/v2
|
||||
// that provides a similar stream-based API for both Unix and Windows systems.
|
||||
// While the Windows API is not technically atomic, it still provides a
|
||||
// consistent stream-based interface, and atomic renames of files do not seem to
|
||||
// be possible in all cases anyway.
|
||||
//
|
||||
// See https://github.com/google/renameio/issues/1.
|
||||
//
|
||||
// TODO(a.garipov): Consider moving to golibs/renameioutil once tried and
|
||||
// tested.
|
||||
package aghrenameio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// PendingFile is the interface for pending temporary files.
|
||||
type PendingFile interface {
|
||||
// Cleanup closes the file, and removes it without performing the renaming.
|
||||
// To close and rename the file, use CloseReplace.
|
||||
Cleanup() (err error)
|
||||
|
||||
// CloseReplace closes the temporary file and replaces the destination file
|
||||
// with it, possibly atomically.
|
||||
//
|
||||
// This method is not safe for concurrent use by multiple goroutines.
|
||||
CloseReplace() (err error)
|
||||
|
||||
// Write writes len(b) bytes from b to the File. It returns the number of
|
||||
// bytes written and an error, if any. Write returns a non-nil error when n
|
||||
// != len(b).
|
||||
Write(b []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// NewPendingFile is a wrapper around [renameio.NewPendingFile] on Unix systems
|
||||
// and [os.CreateTemp] on Windows.
|
||||
func NewPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {
|
||||
return newPendingFile(filePath, mode)
|
||||
}
|
||||
|
||||
// WithDeferredCleanup is a helper that performs the necessary cleanups and
|
||||
// finalizations of the temporary files based on the returned error.
|
||||
func WithDeferredCleanup(returned error, file PendingFile) (err error) {
|
||||
// Make sure that any error returned from here is marked as a deferred one.
|
||||
if returned != nil {
|
||||
return errors.WithDeferred(returned, file.Cleanup())
|
||||
}
|
||||
|
||||
return errors.WithDeferred(nil, file.CloseReplace())
|
||||
}
|
||||
101
internal/aghrenameio/renameio_test.go
Normal file
101
internal/aghrenameio/renameio_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package aghrenameio_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testPerm is the common permission mode for tests.
|
||||
const testPerm fs.FileMode = 0o644
|
||||
|
||||
// Common file data for tests.
|
||||
var (
|
||||
initialData = []byte("initial data\n")
|
||||
newData = []byte("new data\n")
|
||||
)
|
||||
|
||||
func TestPendingFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
targetPath := newInitialFile(t)
|
||||
f, err := aghrenameio.NewPendingFile(targetPath, testPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.Write(newData)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = f.CloseReplace()
|
||||
require.NoError(t, err)
|
||||
|
||||
gotData, err := os.ReadFile(targetPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, newData, gotData)
|
||||
}
|
||||
|
||||
// newInitialFile is a test helper that returns the path to the file containing
|
||||
// [initialData].
|
||||
func newInitialFile(t *testing.T) (targetPath string) {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
targetPath = filepath.Join(dir, "target")
|
||||
|
||||
err := os.WriteFile(targetPath, initialData, 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return targetPath
|
||||
}
|
||||
|
||||
func TestWithDeferredCleanup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const testError errors.Error = "test error"
|
||||
|
||||
testCases := []struct {
|
||||
error error
|
||||
name string
|
||||
wantErrMsg string
|
||||
wantData []byte
|
||||
}{{
|
||||
name: "success",
|
||||
error: nil,
|
||||
wantErrMsg: "",
|
||||
wantData: newData,
|
||||
}, {
|
||||
name: "error",
|
||||
error: testError,
|
||||
wantErrMsg: testError.Error(),
|
||||
wantData: initialData,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
targetPath := newInitialFile(t)
|
||||
f, err := aghrenameio.NewPendingFile(targetPath, testPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.Write(newData)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = aghrenameio.WithDeferredCleanup(tc.error, f)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
gotData, err := os.ReadFile(targetPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantData, gotData)
|
||||
})
|
||||
}
|
||||
}
|
||||
48
internal/aghrenameio/renameio_unix.go
Normal file
48
internal/aghrenameio/renameio_unix.go
Normal file
@@ -0,0 +1,48 @@
|
||||
//go:build unix
|
||||
|
||||
package aghrenameio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/google/renameio/v2"
|
||||
)
|
||||
|
||||
// pendingFile is a wrapper around [*renameio.PendingFile] making it an
|
||||
// [io.WriteCloser].
|
||||
type pendingFile struct {
|
||||
file *renameio.PendingFile
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ PendingFile = pendingFile{}
|
||||
|
||||
// Cleanup implements the [PendingFile] interface for pendingFile.
|
||||
func (f pendingFile) Cleanup() (err error) {
|
||||
return f.file.Cleanup()
|
||||
}
|
||||
|
||||
// CloseReplace implements the [PendingFile] interface for pendingFile.
|
||||
func (f pendingFile) CloseReplace() (err error) {
|
||||
return f.file.CloseAtomicallyReplace()
|
||||
}
|
||||
|
||||
// Write implements the [PendingFile] interface for pendingFile.
|
||||
func (f pendingFile) Write(b []byte) (n int, err error) {
|
||||
return f.file.Write(b)
|
||||
}
|
||||
|
||||
// NewPendingFile is a wrapper around [renameio.NewPendingFile].
|
||||
//
|
||||
// f.Close must be called to finish the renaming.
|
||||
func newPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {
|
||||
file, err := renameio.NewPendingFile(filePath, renameio.WithPermissions(mode))
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pendingFile{
|
||||
file: file,
|
||||
}, nil
|
||||
}
|
||||
74
internal/aghrenameio/renameio_windows.go
Normal file
74
internal/aghrenameio/renameio_windows.go
Normal file
@@ -0,0 +1,74 @@
|
||||
//go:build windows
|
||||
|
||||
package aghrenameio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// pendingFile is a wrapper around [*os.File] calling [os.Rename] in its Close
|
||||
// method.
|
||||
type pendingFile struct {
|
||||
file *os.File
|
||||
targetPath string
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ PendingFile = (*pendingFile)(nil)
|
||||
|
||||
// Cleanup implements the [PendingFile] interface for *pendingFile.
|
||||
func (f *pendingFile) Cleanup() (err error) {
|
||||
closeErr := f.file.Close()
|
||||
err = os.Remove(f.file.Name())
|
||||
|
||||
// Put closeErr into the deferred error because that's where it is usually
|
||||
// expected.
|
||||
return errors.WithDeferred(err, closeErr)
|
||||
}
|
||||
|
||||
// CloseReplace implements the [PendingFile] interface for *pendingFile.
|
||||
func (f *pendingFile) CloseReplace() (err error) {
|
||||
err = f.file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing: %w", err)
|
||||
}
|
||||
|
||||
err = os.Rename(f.file.Name(), f.targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write implements the [PendingFile] interface for *pendingFile.
|
||||
func (f *pendingFile) Write(b []byte) (n int, err error) {
|
||||
return f.file.Write(b)
|
||||
}
|
||||
|
||||
// NewPendingFile is a wrapper around [os.CreateTemp].
|
||||
//
|
||||
// f.Close must be called to finish the renaming.
|
||||
func newPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {
|
||||
// Use the same directory as the file itself, because moves across
|
||||
// filesystems can be especially problematic.
|
||||
file, err := os.CreateTemp(filepath.Dir(filePath), "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening pending file: %w", err)
|
||||
}
|
||||
|
||||
err = file.Chmod(mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("preparing pending file: %w", err)
|
||||
}
|
||||
|
||||
return &pendingFile{
|
||||
file: file,
|
||||
targetPath: filePath,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user