From d5fa746ed1ed31b664cc30fab8c7a76702892c1d Mon Sep 17 00:00:00 2001 From: Fazlul Shahriar Date: Wed, 29 Jul 2020 15:54:20 -0400 Subject: [PATCH] Add os.Rename wrapper for Plan 9 (#87) os.Rename documentation says: "OS-specific restrictions may apply when oldpath and newpath are in different directories." On Unix, this means we can't rename across devices. On Plan 9 however, the functionality is even more limited: cross-directory renames are not allowed at all. Add a wrapper around os.Rename for Plan 9, which will copy the file if we're renaming across directory. All tests seems to pass. (Aside: I also had to write this wrapper to get go-git working on Plan 9: https://github.com/go-git/go-billy/blob/v5.0.0/osfs/os_plan9.go#L27 but I notice few issues with that one.) Fixes #86 --- convert.go | 4 +-- flatfs.go | 4 +-- rename.go | 7 +++++ rename_plan9.go | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 rename.go create mode 100644 rename_plan9.go diff --git a/convert.go b/convert.go index 2df5ccb..58a12fb 100644 --- a/convert.go +++ b/convert.go @@ -156,7 +156,7 @@ func Move(oldPath string, newPath string, out io.Writer) error { // else we found something unexpected, so to be safe just move it log.Warnw("found unexpected file in datastore directory, moving anyways", "file", fn) newPath := filepath.Join(newDS.path, fn) - err := os.Rename(oldPath, newPath) + err := rename(oldPath, newPath) if err != nil { return err } @@ -177,7 +177,7 @@ func moveKey(oldDS *Datastore, newDS *Datastore, key datastore.Key) error { if err != nil && !os.IsExist(err) { return err } - err = os.Rename(oldPath, newPath) + err = rename(oldPath, newPath) if err != nil { return err } diff --git a/flatfs.go b/flatfs.go index 9d372cd..40be67d 100644 --- a/flatfs.go +++ b/flatfs.go @@ -369,7 +369,7 @@ func (fs *Datastore) renameAndUpdateDiskUsage(tmpPath, path string) error { // it will either a) Re-add the size of an existing file, which // was sustracted before b) Add 0 if there is no existing file. for i := 0; i < RetryAttempts; i++ { - err = os.Rename(tmpPath, path) + err = rename(tmpPath, path) // if there's no error, or the source file doesn't exist, abort. if err == nil || os.IsNotExist(err) { break @@ -1060,7 +1060,7 @@ func (fs *Datastore) writeDiskUsageFile(du int64, doSync bool) { } closed = true - if err := os.Rename(tmp.Name(), filepath.Join(fs.path, DiskUsageFile)); err != nil { + if err := rename(tmp.Name(), filepath.Join(fs.path, DiskUsageFile)); err != nil { log.Warnw("cound not write disk usage", "error", err) return } diff --git a/rename.go b/rename.go new file mode 100644 index 0000000..da458b1 --- /dev/null +++ b/rename.go @@ -0,0 +1,7 @@ +// +build !plan9 + +package flatfs + +import "os" + +var rename = os.Rename diff --git a/rename_plan9.go b/rename_plan9.go new file mode 100644 index 0000000..c32a59a --- /dev/null +++ b/rename_plan9.go @@ -0,0 +1,77 @@ +package flatfs + +import ( + "io" + "os" + "path/filepath" + "syscall" +) + +// rename behaves like os.Rename but can rename files across directories. +func rename(oldpath, newpath string) error { + err := os.Rename(oldpath, newpath) + if le, ok := err.(*os.LinkError); !ok || le.Err != os.ErrInvalid { + return err + } + if filepath.Dir(oldpath) == filepath.Dir(newpath) { + // We should not get here, but just in case + // os.ErrInvalid is used for something else in the future. + return err + } + + src, err := os.Open(oldpath) + if err != nil { + return &os.LinkError{"rename", oldpath, newpath, err} + } + defer src.Close() + + fi, err := src.Stat() + if err != nil { + return &os.LinkError{"rename", oldpath, newpath, err} + } + if fi.Mode().IsDir() { + return &os.LinkError{"rename", oldpath, newpath, syscall.EISDIR} + } + + dst, err := os.OpenFile(newpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) + if err != nil { + return &os.LinkError{"rename", oldpath, newpath, err} + } + + if _, err := io.Copy(dst, src); err != nil { + dst.Close() + os.Remove(newpath) + return &os.LinkError{"rename", oldpath, newpath, err} + } + if err := dst.Close(); err != nil { + os.Remove(newpath) + return &os.LinkError{"rename", oldpath, newpath, err} + } + + // Copy mtime and mode from original file. + // We need only one syscall if we avoid os.Chmod and os.Chtimes. + dir := fi.Sys().(*syscall.Dir) + var d syscall.Dir + d.Null() + d.Mtime = dir.Mtime + d.Mode = dir.Mode + _ = dirwstat(newpath, &d) // ignore error, as per mv(1) + + if err := os.Remove(oldpath); err != nil { + return &os.LinkError{"rename", oldpath, newpath, err} + } + return nil +} + +func dirwstat(name string, d *syscall.Dir) error { + var buf [syscall.STATFIXLEN]byte + + n, err := d.Marshal(buf[:]) + if err != nil { + return &os.PathError{"dirwstat", name, err} + } + if err = syscall.Wstat(name, buf[:n]); err != nil { + return &os.PathError{"dirwstat", name, err} + } + return nil +}