diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 13b2d8def..3f945bc71 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -85,6 +85,7 @@ type BackupOptions struct { FilesFrom []string TimeStamp string WithAtime bool + IgnoreInode bool } var backupOptions BackupOptions @@ -112,6 +113,7 @@ func init() { f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from file (can be combined with file args/can be specified multiple times)") f.StringVar(&backupOptions.TimeStamp, "time", "", "time of the backup (ex. '2012-11-01 22:08:41') (default: now)") f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories") + f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number changes when checking for modified files") } // filterExisting returns a slice of all existing items, or an error if no @@ -549,6 +551,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina arch.CompleteItem = p.CompleteItem arch.StartFile = p.StartFile arch.CompleteBlob = p.CompleteBlob + arch.IgnoreInode = opts.IgnoreInode if parentSnapshotID == nil { parentSnapshotID = &restic.ID{} diff --git a/doc/040_backup.rst b/doc/040_backup.rst index 3513e5854..32ce21260 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -279,6 +279,10 @@ written, and the next backup needs to write new metadata again. If you really want to save the access time for files and directories, you can pass the ``--with-atime`` option to the ``backup`` command. +In filesystems that do not support inode consistency, like FUSE-based ones and pCloud, it is +possible to ignore inode on changed files comparison by passing ``--ignore-inode`` to +``backup`` command. + Reading data from stdin *********************** diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index 4ce9ef597..d93ea3986 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -79,6 +79,7 @@ type Archiver struct { // be saved. Enabling it may result in much metadata, so it's off by // default. WithAtime bool + IgnoreInode bool } // Options is used to configure the archiver. @@ -133,6 +134,7 @@ func New(repo restic.Repository, fs fs.FS, opts Options) *Archiver { CompleteItem: func(string, *restic.Node, *restic.Node, ItemStats, time.Duration) {}, StartFile: func(string) {}, CompleteBlob: func(string, uint64) {}, + IgnoreInode: false, } return arch @@ -383,7 +385,7 @@ func (arch *Archiver) Save(ctx context.Context, snPath, target string, previous } // use previous node if the file hasn't changed - if previous != nil && !fileChanged(fi, previous) { + if previous != nil && !fileChanged(fi, previous, arch.IgnoreInode) { debug.Log("%v hasn't changed, returning old node", target) arch.CompleteItem(snPath, previous, previous, ItemStats{}, time.Since(start)) arch.CompleteBlob(snPath, previous.Size) @@ -436,7 +438,7 @@ func (arch *Archiver) Save(ctx context.Context, snPath, target string, previous // fileChanged returns true if the file's content has changed since the node // was created. -func fileChanged(fi os.FileInfo, node *restic.Node) bool { +func fileChanged(fi os.FileInfo, node *restic.Node, ignoreInode bool) bool { if node == nil { return true } @@ -458,7 +460,7 @@ func fileChanged(fi os.FileInfo, node *restic.Node) bool { } // check inode - if node.Inode != extFI.Inode { + if !ignoreInode && node.Inode != extFI.Inode { return true } diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go index 8749dcc81..51c499c0c 100644 --- a/internal/archiver/archiver_test.go +++ b/internal/archiver/archiver_test.go @@ -555,9 +555,11 @@ func TestFileChanged(t *testing.T) { } var tests = []struct { - Name string - Content []byte - Modify func(t testing.TB, filename string) + Name string + Content []byte + Modify func(t testing.TB, filename string) + IgnoreInode bool + Check bool }{ { Name: "same-content-new-file", @@ -596,6 +598,18 @@ func TestFileChanged(t *testing.T) { save(t, filename, defaultContent) }, }, + { + Name: "ignore-inode", + Modify: func(t testing.TB, filename string) { + fi := lstat(t, filename) + remove(t, filename) + sleep() + save(t, filename, defaultContent) + setTimestamp(t, filename, fi.ModTime(), fi.ModTime()) + }, + IgnoreInode: true, + Check: true, + }, } for _, test := range tests { @@ -613,15 +627,19 @@ func TestFileChanged(t *testing.T) { fiBefore := lstat(t, filename) node := nodeFromFI(t, filename, fiBefore) - if fileChanged(fiBefore, node) { + if fileChanged(fiBefore, node, false) { t.Fatalf("unchanged file detected as changed") } test.Modify(t, filename) fiAfter := lstat(t, filename) - if !fileChanged(fiAfter, node) { - t.Fatalf("modified file detected as unchanged") + if test.Check == fileChanged(fiAfter, node, test.IgnoreInode) { + if test.Check { + t.Fatalf("unmodified file detected as changed") + } else { + t.Fatalf("modified file detected as unchanged") + } } }) } @@ -637,7 +655,7 @@ func TestFilChangedSpecialCases(t *testing.T) { t.Run("nil-node", func(t *testing.T) { fi := lstat(t, filename) - if !fileChanged(fi, nil) { + if !fileChanged(fi, nil, false) { t.Fatal("nil node detected as unchanged") } }) @@ -646,7 +664,7 @@ func TestFilChangedSpecialCases(t *testing.T) { fi := lstat(t, filename) node := nodeFromFI(t, filename, fi) node.Type = "symlink" - if !fileChanged(fi, node) { + if !fileChanged(fi, node, false) { t.Fatal("node with changed type detected as unchanged") } })