diff --git a/changelog/unreleased/pull-2816 b/changelog/unreleased/pull-2816 new file mode 100644 index 000000000..4033a17ab --- /dev/null +++ b/changelog/unreleased/pull-2816 @@ -0,0 +1,10 @@ +Enhancement: backup no longer updates file access times on Linux + +When reading files during backup, restic caused the operating system to update +the file access times. Note that this does not apply to filesystems with +disabled file access times. + +Restic now instructs the operation system not to update the file access time, +if the user running restic is the file owner or has root permissions. + +https://github.com/restic/restic/pull/2816 diff --git a/internal/fs/fs_local.go b/internal/fs/fs_local.go index dd1faafa0..48c40dc90 100644 --- a/internal/fs/fs_local.go +++ b/internal/fs/fs_local.go @@ -24,6 +24,7 @@ func (fs Local) Open(name string) (File, error) { if err != nil { return nil, err } + _ = setFlags(f) return f, nil } @@ -37,6 +38,7 @@ func (fs Local) OpenFile(name string, flag int, perm os.FileMode) (File, error) if err != nil { return nil, err } + _ = setFlags(f) return f, nil } 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 +}