Skip to content

Commit

Permalink
Add os.Rename wrapper for Plan 9 (#87)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
fhs authored Jul 29, 2020
1 parent 5aadd5c commit d5fa746
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 4 deletions.
4 changes: 2 additions & 2 deletions convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions flatfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions rename.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build !plan9

package flatfs

import "os"

var rename = os.Rename
77 changes: 77 additions & 0 deletions rename_plan9.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit d5fa746

Please sign in to comment.