mirror of
https://github.com/mjl-/mox.git
synced 2025-07-15 10:14:36 +03:00
move "link or copy" functionality to moxio
and add a bit more logging for unexpected failures when closing files. and make tests pass with a TMPDIR on a different filesystem than the testdata directory.
This commit is contained in:
71
moxio/linkcopy.go
Normal file
71
moxio/linkcopy.go
Normal file
@ -0,0 +1,71 @@
|
||||
package moxio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/mjl-/mox/mlog"
|
||||
)
|
||||
|
||||
// LinkOrCopy attempts to make a hardlink dst. If that fails, it will try to do a
|
||||
// regular file copy. If srcReaderOpt is not nil, it will be used for reading. If
|
||||
// sync is true and the file is copied, Sync is called on the file after writing to
|
||||
// ensure the file is written on disk. Callers should also sync the directory of
|
||||
// the destination file, but may want to do that after linking/copying multiple
|
||||
// files. If dst was created and an error occurred, it is removed.
|
||||
func LinkOrCopy(log *mlog.Log, dst, src string, srcReaderOpt io.Reader, sync bool) (rerr error) {
|
||||
// Try hardlink first.
|
||||
err := os.Link(src, dst)
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if os.IsNotExist(err) {
|
||||
// No point in trying with regular copy, we would fail again. Either src doesn't
|
||||
// exist or dst directory doesn't exist.
|
||||
return err
|
||||
}
|
||||
|
||||
// File system may not support hardlinks, or link could be crossing file systems.
|
||||
// Do a regular file copy.
|
||||
if srcReaderOpt == nil {
|
||||
sf, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := sf.Close()
|
||||
log.Check(err, "closing copied source file")
|
||||
}()
|
||||
srcReaderOpt = sf
|
||||
}
|
||||
|
||||
df, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0660)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create destination: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if df != nil {
|
||||
err = os.Remove(dst)
|
||||
log.Check(err, "removing partial destination file")
|
||||
err = df.Close()
|
||||
log.Check(err, "closing partial destination file")
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(df, srcReaderOpt); err != nil {
|
||||
return fmt.Errorf("copy: %w", err)
|
||||
}
|
||||
if sync {
|
||||
if err := df.Sync(); err != nil {
|
||||
return fmt.Errorf("sync destination: %w", err)
|
||||
}
|
||||
}
|
||||
err = df.Close()
|
||||
df = nil
|
||||
if err != nil {
|
||||
err := os.Remove(dst)
|
||||
log.Check(err, "removing partial destination file")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
57
moxio/linkcopy_test.go
Normal file
57
moxio/linkcopy_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package moxio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mjl-/mox/mlog"
|
||||
)
|
||||
|
||||
func tcheckf(t *testing.T, err error, format string, args ...any) {
|
||||
if err != nil {
|
||||
t.Helper()
|
||||
t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkOrCopy(t *testing.T) {
|
||||
log := mlog.New("linkorcopy")
|
||||
|
||||
// link in same directory. file exists error. link to file in non-existent
|
||||
// directory (exists error). link to file in system temp dir (hopefully other file
|
||||
// system).
|
||||
src := "linkorcopytest-src.txt"
|
||||
f, err := os.Create(src)
|
||||
tcheckf(t, err, "creating test file")
|
||||
defer os.Remove(src)
|
||||
err = LinkOrCopy(log, "linkorcopytest-dst.txt", src, nil, false)
|
||||
tcheckf(t, err, "linking file")
|
||||
err = os.Remove("linkorcopytest-dst.txt")
|
||||
tcheckf(t, err, "remove dst")
|
||||
|
||||
err = LinkOrCopy(log, "bogus/linkorcopytest-dst.txt", src, nil, false)
|
||||
if err == nil || !os.IsNotExist(err) {
|
||||
t.Fatalf("expected is not exist, got %v", err)
|
||||
}
|
||||
|
||||
// Try with copying the file. This can currently only really happen on systems that
|
||||
// don't support hardlinking. Because other code and tests already use os.Rename on
|
||||
// similar files, which will fail for being cross-filesystem (and we do want
|
||||
// users/admins to have the mox temp dir on the same file system as the account
|
||||
// files).
|
||||
dst := filepath.Join(os.TempDir(), "linkorcopytest-dst.txt")
|
||||
err = LinkOrCopy(log, dst, src, nil, true)
|
||||
tcheckf(t, err, "copy file")
|
||||
err = os.Remove(dst)
|
||||
tcheckf(t, err, "removing dst")
|
||||
|
||||
// Copy based on open file.
|
||||
_, err = f.Seek(0, 0)
|
||||
tcheckf(t, err, "seek to start")
|
||||
err = LinkOrCopy(log, dst, src, f, true)
|
||||
tcheckf(t, err, "copy file from reader")
|
||||
err = os.Remove(dst)
|
||||
tcheckf(t, err, "removing dst")
|
||||
}
|
Reference in New Issue
Block a user