diff --git a/changelog/unreleased/issue-2179 b/changelog/unreleased/issue-2179 new file mode 100644 index 000000000..c0a275b04 --- /dev/null +++ b/changelog/unreleased/issue-2179 @@ -0,0 +1,8 @@ +Bugfix: Use ctime when checking for file changes + +Previously, restic only checked a file's mtime (along with other non-timestamp +data) to decide if a file has changed. This could cause it to not notice changes +if something edits a file and then resets the timestamp. Restic now also checks +the ctime, so any modification to a file should be noticed. + +https://github.com/restic/restic/issues/2179 diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index b21f79e87..e612ab8e7 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -453,8 +453,13 @@ func fileChanged(fi os.FileInfo, node *restic.Node, ignoreInode bool) bool { return true } - // check size + // check status change timestamp extFI := fs.ExtendedStat(fi) + if !extFI.ChangeTime.Equal(node.ChangeTime) { + return true + } + + // check size if uint64(fi.Size()) != node.Size || uint64(extFI.Size) != node.Size { return true } diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go index 419b3a91c..ba22b8abe 100644 --- a/internal/archiver/archiver_test.go +++ b/internal/archiver/archiver_test.go @@ -1,6 +1,7 @@ package archiver import ( + "bytes" "context" "io/ioutil" "os" @@ -576,6 +577,26 @@ func TestFileChanged(t *testing.T) { save(t, filename, defaultContent) }, }, + { + Name: "new-content-same-timestamp", + Modify: func(t testing.TB, filename string) { + fi, _ := os.Stat(filename) + extFI := fs.ExtendedStat(fi) + save(t, filename, bytes.ToUpper(defaultContent)) + sleep() + ts := []syscall.Timespec{ + { + Sec: extFI.AccessTime.Unix(), + Nsec: int64(extFI.AccessTime.Nanosecond()), + }, + { + Sec: extFI.ModTime.Unix(), + Nsec: int64(extFI.ModTime.Nanosecond()), + }, + } + syscall.UtimesNano(filename, ts) + }, + }, { Name: "other-content", Modify: func(t testing.TB, filename string) { diff --git a/internal/fs/stat.go b/internal/fs/stat.go index d37d12942..e1006fd61 100644 --- a/internal/fs/stat.go +++ b/internal/fs/stat.go @@ -22,6 +22,7 @@ type ExtendedFileInfo struct { AccessTime time.Time // last access time stamp ModTime time.Time // last (content) modification time stamp + ChangeTime time.Time // last status change time stamp } // ExtendedStat returns an ExtendedFileInfo constructed from the os.FileInfo. diff --git a/internal/fs/stat_bsd.go b/internal/fs/stat_bsd.go index 62a258e64..d5e8ce550 100644 --- a/internal/fs/stat_bsd.go +++ b/internal/fs/stat_bsd.go @@ -30,6 +30,7 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo { AccessTime: time.Unix(s.Atimespec.Unix()), ModTime: time.Unix(s.Mtimespec.Unix()), + ChangeTime: time.Unix(s.Ctimespec.Unix()), } return extFI diff --git a/internal/fs/stat_unix.go b/internal/fs/stat_unix.go index 56c22f8bc..34b98a31e 100644 --- a/internal/fs/stat_unix.go +++ b/internal/fs/stat_unix.go @@ -30,6 +30,7 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo { AccessTime: time.Unix(s.Atim.Unix()), ModTime: time.Unix(s.Mtim.Unix()), + ChangeTime: time.Unix(s.Ctim.Unix()), } return extFI diff --git a/internal/fs/stat_windows.go b/internal/fs/stat_windows.go index 16f9fe0eb..a8f13ccea 100644 --- a/internal/fs/stat_windows.go +++ b/internal/fs/stat_windows.go @@ -27,5 +27,7 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo { mtime := syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds()) extFI.ModTime = time.Unix(mtime.Unix()) + extFI.ChangeTime = extFI.ModTime + return extFI }