From 72e3f53c8f4de5582b63862b9b9e84f1d8a0756d Mon Sep 17 00:00:00 2001 From: davidby-influx <72418212+davidby-influx@users.noreply.github.com> Date: Wed, 12 Oct 2022 09:50:27 -0700 Subject: [PATCH] fix: use copy when a rename spans volumes (#23785) When a file rename fails with EXDEV (cross device or volume error), copy the file and delete the original instead Differs from master branch by overwriting existing files instead of erring. closes https://github.com/influxdata/influxdb/issues/22997 --- pkg/file/file_unix.go | 64 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/pkg/file/file_unix.go b/pkg/file/file_unix.go index 66609888e58..41bc0b5f71a 100644 --- a/pkg/file/file_unix.go +++ b/pkg/file/file_unix.go @@ -3,8 +3,13 @@ package file import ( + "errors" + "fmt" + "io" "os" "syscall" + + errors2 "github.com/influxdata/influxdb/pkg/errors" ) func SyncDir(dirName string) error { @@ -29,7 +34,62 @@ func SyncDir(dirName string) error { return dir.Close() } -// RenameFile will rename the source to target using os function. +// RenameFileWithReplacement will replace any existing file at newpath with the contents +// of oldpath. It works also if it the rename spans over several file systems. +// +// If no file already exists at newpath, newpath will be created using the contents +// of oldpath. If this function returns successfully, the contents of newpath will +// be identical to oldpath, and oldpath will be removed. +func RenameFileWithReplacement(oldpath, newpath string) error { + if err := os.Rename(oldpath, newpath); !errors.Is(err, syscall.EXDEV) { + // note: also includes err == nil + return err + } + + // move over filesystem boundaries, we have to copy. + // (if there was another error, it will likely fail a second time) + return MoveFileWithReplacement(oldpath, newpath) + +} + +// RenameFile renames oldpath to newpath, returning an error if newpath already +// exists. If this function returns successfully, the contents of newpath will +// be identical to oldpath, and oldpath will be removed. func RenameFile(oldpath, newpath string) error { - return os.Rename(oldpath, newpath) + return RenameFileWithReplacement(oldpath, newpath) +} + +func copyFile(src, dst string) (err error) { + in, err := os.Open(src) + if err != nil { + return err + } + + out, err := os.Create(dst) + if err != nil { + return err + } + + defer errors2.Capture(&err, out.Close)() + + defer errors2.Capture(&err, in.Close)() + + if _, err = io.Copy(out, in); err != nil { + return err + } + + return out.Sync() +} + +// MoveFileWithReplacement copies the file contents at `src` to `dst`. +// and deletes `src` on success. +// +// If the file at `dst` already exists, it will be truncated and its contents +// overwritten. +func MoveFileWithReplacement(src, dst string) error { + if err := copyFile(src, dst); err != nil { + return fmt.Errorf("copy: %w", err) + } + + return os.Remove(src) }