Simplify atomic file replacement and bounded reads
This commit is contained in:
@@ -12,9 +12,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrDirectorySyncUnsupported reports that the platform or filesystem cannot
|
||||
// make a renamed directory entry crash-durable. The replacement may be visible,
|
||||
// but callers must not treat it as having the same durability guarantee.
|
||||
// ErrDirectorySyncUnsupported remains for legacy confined migration paths.
|
||||
// The retained replacement entry points do not perform directory syncing.
|
||||
var ErrDirectorySyncUnsupported = errors.New("directory sync is unsupported")
|
||||
|
||||
// ReplaceFileOptions describes the caller-owned parts of a byte-file
|
||||
@@ -26,13 +25,10 @@ type ReplaceFileOptions struct {
|
||||
BeforeRename func() error
|
||||
}
|
||||
|
||||
// ReplaceFileAtomic durably replaces dst with data. It creates a sibling
|
||||
// temporary file, writes and syncs its bytes and mode, atomically renames it,
|
||||
// then syncs the destination parent. On platforms where replacement of an
|
||||
// existing destination or directory syncing is unavailable, it returns an
|
||||
// error rather than claiming an equivalent durability guarantee.
|
||||
// ReplaceFileAtomic replaces dst with data through a sibling temporary file.
|
||||
// It provides atomic visibility, not a crash- or power-loss-durability claim.
|
||||
func ReplaceFileAtomic(dst string, data []byte, options ReplaceFileOptions) error {
|
||||
return replaceFileFromReaderConfined(dst, bytes.NewReader(data), options)
|
||||
return replaceFileFromReader(dst, bytes.NewReader(data), options)
|
||||
}
|
||||
|
||||
// WriteFileAtomic creates the destination parent with workspace permissions
|
||||
@@ -85,7 +81,7 @@ func WriteReaderAtomicWithChecksum(dst string, src io.Reader, perm os.FileMode)
|
||||
}
|
||||
|
||||
digest := sha256.New()
|
||||
if err := replaceFileFromReaderConfined(
|
||||
if err := replaceFileFromReader(
|
||||
dst,
|
||||
io.TeeReader(src, digest),
|
||||
ReplaceFileOptions{Mode: perm},
|
||||
@@ -95,6 +91,46 @@ func WriteReaderAtomicWithChecksum(dst string, src io.Reader, perm os.FileMode)
|
||||
return hex.EncodeToString(digest.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func replaceFileFromReader(dst string, source io.Reader, options ReplaceFileOptions) (resultErr error) {
|
||||
if strings.TrimSpace(dst) == "" {
|
||||
return fmt.Errorf("destination path is required")
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(dst), "."+filepath.Base(dst)+".tmp-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
removeTmp := true
|
||||
defer func() {
|
||||
if removeTmp {
|
||||
if err := os.Remove(tmpPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("remove temporary file: %w", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
if _, err := io.Copy(tmp, source); err != nil {
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("write temp file: %w", err)
|
||||
}
|
||||
if err := tmp.Chmod(options.Mode); err != nil {
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("set temp file permissions: %w", err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return fmt.Errorf("close temp file: %w", err)
|
||||
}
|
||||
if options.BeforeRename != nil {
|
||||
if err := options.BeforeRename(); err != nil {
|
||||
return fmt.Errorf("check before file replacement: %w", err)
|
||||
}
|
||||
}
|
||||
if err := os.Rename(tmpPath, dst); err != nil {
|
||||
return fmt.Errorf("install temp file: %w", err)
|
||||
}
|
||||
removeTmp = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallDownloadedTempFile installs a fully downloaded sibling temporary file
|
||||
// at dst. On failure before the rename, the caller retains ownership of tmpPath
|
||||
// and is responsible for cleanup.
|
||||
|
||||
Reference in New Issue
Block a user