diff --git a/internal/fs/file.go b/internal/fs/file.go index e8e9080d7..a7c2b8886 100644 --- a/internal/fs/file.go +++ b/internal/fs/file.go @@ -85,7 +85,12 @@ func Create(name string) (*os.File, error) { // Open opens a file for reading. func Open(name string) (File, error) { - return os.Open(fixpath(name)) + f, err := os.Open(fixpath(name)) + if err != nil { + return nil, err + } + setFlags(f) + return f, err } // OpenFile is the generalized open call; most users will use Open @@ -94,7 +99,12 @@ func Open(name string) (File, error) { // methods on the returned File can be used for I/O. // If there is an error, it will be of type *PathError. func OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) { - return os.OpenFile(fixpath(name), flag, perm) + f, err := os.OpenFile(fixpath(name), flag, perm) + if err != nil { + return nil, err + } + setFlags(f) + return f, err } // Walk walks the file tree rooted at root, calling walkFn for each file or diff --git a/internal/fs/setflags_linux.go b/internal/fs/setflags_linux.go new file mode 100644 index 000000000..32e3d2683 --- /dev/null +++ b/internal/fs/setflags_linux.go @@ -0,0 +1,21 @@ +package fs + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// SetFlags tries to set the O_NOATIME flag on f, which prevents the kernel +// from updating the atime on a read call. +// +// The call fails when we're not the owner of the file or root. The caller +// should ignore the error, which is returned for testing only. +func setFlags(f *os.File) error { + fd := f.Fd() + flags, err := unix.FcntlInt(fd, unix.F_GETFL, 0) + if err == nil { + _, err = unix.FcntlInt(fd, unix.F_SETFL, flags|unix.O_NOATIME) + } + return err +} diff --git a/internal/fs/setflags_linux_test.go b/internal/fs/setflags_linux_test.go new file mode 100644 index 000000000..7818146ac --- /dev/null +++ b/internal/fs/setflags_linux_test.go @@ -0,0 +1,67 @@ +package fs + +import ( + "io" + "io/ioutil" + "os" + "testing" + "time" + + rtest "github.com/restic/restic/internal/test" + + "golang.org/x/sys/unix" +) + +func TestNoatime(t *testing.T) { + f, err := ioutil.TempFile("", "restic-test-noatime") + if err != nil { + t.Fatal(err) + } + + defer func() { + _ = f.Close() + err = Remove(f.Name()) + if err != nil { + t.Fatal(err) + } + }() + + // Only run this test on common filesystems that support O_NOATIME. + // On others, we may not get an error. + if !supportsNoatime(t, f) { + t.Skip("temp directory may not support O_NOATIME, skipping") + } + // From this point on, we own the file, so we should not get EPERM. + + _, err = io.WriteString(f, "Hello!") + rtest.OK(t, err) + _, err = f.Seek(0, io.SeekStart) + rtest.OK(t, err) + + getAtime := func() time.Time { + info, err := f.Stat() + rtest.OK(t, err) + return ExtendedStat(info).AccessTime + } + + atime := getAtime() + + err = setFlags(f) + rtest.OK(t, err) + + _, err = f.Read(make([]byte, 1)) + rtest.OK(t, err) + rtest.Equals(t, atime, getAtime()) +} + +func supportsNoatime(t *testing.T, f *os.File) bool { + var fsinfo unix.Statfs_t + err := unix.Fstatfs(int(f.Fd()), &fsinfo) + rtest.OK(t, err) + + return fsinfo.Type == unix.BTRFS_SUPER_MAGIC || + fsinfo.Type == unix.EXT2_SUPER_MAGIC || + fsinfo.Type == unix.EXT3_SUPER_MAGIC || + fsinfo.Type == unix.EXT4_SUPER_MAGIC || + fsinfo.Type == unix.TMPFS_MAGIC +} diff --git a/internal/fs/setflags_other.go b/internal/fs/setflags_other.go new file mode 100644 index 000000000..6485126e0 --- /dev/null +++ b/internal/fs/setflags_other.go @@ -0,0 +1,12 @@ +//go:build !linux +// +build !linux + +package fs + +import "os" + +// OS-specific replacements of setFlags can set file status flags +// that improve I/O performance. +func setFlags(*os.File) error { + return nil +}