Merge pull request #4816 from MichaelEischer/skip-if-unchanged

backup: add support for `--skip-if-unchanged`
This commit is contained in:
Michael Eischer 2024-05-30 15:39:08 +02:00 committed by GitHub
commit 7e0ee5974f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 122 additions and 42 deletions

View file

@ -0,0 +1,11 @@
Enhancement: `backup` can omit snapshot creation if there was no change
The `backup` command always created a snapshot even if nothing changed
compared to the parent snapshot.
Restic now supports the `--skip-if-unchanged` option for the `backup`
command to omit creating a snapshot if the new snapshot's content would
be identical to that of the parent snapshot.
https://github.com/restic/restic/issues/662
https://github.com/restic/restic/pull/4816

View file

@ -87,6 +87,7 @@ type BackupOptions struct {
DryRun bool
ReadConcurrency uint
NoScan bool
SkipIfUnchanged bool
}
var backupOptions BackupOptions
@ -133,6 +134,7 @@ func init() {
if runtime.GOOS == "windows" {
f.BoolVar(&backupOptions.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
}
f.BoolVar(&backupOptions.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot")
// parse read concurrency from env, on error the default value will be used
readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32)
@ -638,13 +640,14 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
}
snapshotOpts := archiver.SnapshotOptions{
Excludes: opts.Excludes,
Tags: opts.Tags.Flatten(),
BackupStart: backupStart,
Time: timeStamp,
Hostname: opts.Host,
ParentSnapshot: parentSnapshot,
ProgramVersion: "restic " + version,
Excludes: opts.Excludes,
Tags: opts.Tags.Flatten(),
BackupStart: backupStart,
Time: timeStamp,
Hostname: opts.Host,
ParentSnapshot: parentSnapshot,
ProgramVersion: "restic " + version,
SkipIfUnchanged: opts.SkipIfUnchanged,
}
if !gopts.JSON {
@ -665,9 +668,6 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
// Report finished execution
progressReporter.Finish(id, summary, opts.DryRun)
if !gopts.JSON && !opts.DryRun {
progressPrinter.P("snapshot %s saved\n", id.Str())
}
if !success {
return ErrInvalidSourceData
}

View file

@ -641,3 +641,18 @@ func TestBackupEmptyPassword(t *testing.T) {
testListSnapshots(t, env.gopts, 1)
testRunCheck(t, env.gopts)
}
func TestBackupSkipIfUnchanged(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
opts := BackupOptions{SkipIfUnchanged: true}
for i := 0; i < 3; i++ {
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
testListSnapshots(t, env.gopts, 1)
}
testRunCheck(t, env.gopts)
}

View file

@ -24,16 +24,17 @@ again:
$ restic -r /srv/restic-repo --verbose backup ~/work
open repository
enter password for repository:
password is correct
lock repository
repository a14e5863 opened (version 2, compression level auto)
load index files
start scan
start backup
scan finished in 1.837s
processed 1.720 GiB in 0:12
start scan on [/home/user/work]
start backup on [/home/user/work]
scan finished in 1.837s: 5307 files, 1.720 GiB
Files: 5307 new, 0 changed, 0 unmodified
Dirs: 1867 new, 0 changed, 0 unmodified
Added: 1.200 GiB
Added to the repository: 1.200 GiB (1.103 GiB stored)
processed 5307 files, 1.720 GiB in 0:12
snapshot 40dc1520 saved
As you can see, restic created a backup of the directory and was pretty
@ -44,6 +45,7 @@ You can see that restic tells us it processed 1.720 GiB of data, this is the
size of the files and directories in ``~/work`` on the local file system. It
also tells us that only 1.200 GiB was added to the repository. This means that
some of the data was duplicate and restic was able to efficiently reduce it.
The data compression also managed to compress the data down to 1.103 GiB.
If you don't pass the ``--verbose`` option, restic will print less data. You'll
still get a nice live status display. Be aware that the live status shows the
@ -109,17 +111,18 @@ repository (since all data is already there). This is de-duplication at work!
$ restic -r /srv/restic-repo --verbose backup ~/work
open repository
enter password for repository:
password is correct
lock repository
repository a14e5863 opened (version 2, compression level auto)
load index files
using parent snapshot d875ae93
start scan
start backup
scan finished in 1.881s
processed 1.720 GiB in 0:03
using parent snapshot 40dc1520
start scan on [/home/user/work]
start backup on [/home/user/work]
scan finished in 1.881s: 5307 files, 1.720 GiB
Files: 0 new, 0 changed, 5307 unmodified
Dirs: 0 new, 0 changed, 1867 unmodified
Added: 0 B
Added to the repository: 0 B (0 B stored)
processed 5307 files, 1.720 GiB in 0:03
snapshot 79766175 saved
You can even backup individual files in the same repository (not passing
@ -129,7 +132,6 @@ You can even backup individual files in the same repository (not passing
$ restic -r /srv/restic-repo backup ~/work.txt
enter password for repository:
password is correct
snapshot 249d0210 saved
If you're interested in what restic does, pass ``--verbose`` twice (or
@ -143,7 +145,6 @@ restic encounters:
$ restic -r /srv/restic-repo --verbose --verbose backup ~/work.txt
open repository
enter password for repository:
password is correct
lock repository
load index files
using parent snapshot f3f8d56b
@ -231,6 +232,40 @@ On **Windows**, a file is considered unchanged when its path, size
and modification time match, and only ``--force`` has any effect.
The other options are recognized but ignored.
Skip creating snapshots if unchanged
************************************
By default, restic always creates a new snapshot even if nothing has changed
compared to the parent snapshot. To omit the creation of a new snapshot in this
case, specify the ``--skip-if-unchanged`` option.
Note that when using absolute paths to specify the backup target, then also
changes to the parent folders result in a changed snapshot. For example, a backup
of ``/home/user/work`` will create a new snapshot if the metadata of either
``/``, ``/home`` or ``/home/user`` change. To avoid this problem run restic from
the corresponding folder and use relative paths.
.. code-block:: console
$ cd /home/user/work && restic -r /srv/restic-repo backup . --skip-if-unchanged
open repository
enter password for repository:
repository a14e5863 opened (version 2, compression level auto)
load index files
using parent snapshot 40dc1520
start scan on [.]
start backup on [.]
scan finished in 1.814s: 5307 files, 1.720 GiB
Files: 0 new, 0 changed, 5307 unmodified
Dirs: 0 new, 0 changed, 1867 unmodified
Added to the repository: 0 B (0 B stored)
processed 5307 files, 1.720 GiB in 0:03
skipped creating snapshot
Dry Runs
********
@ -469,7 +504,6 @@ and displays a small statistic, just pass the command two snapshot IDs:
.. code-block:: console
$ restic -r /srv/restic-repo diff 5845b002 2ab627a6
password is correct
comparing snapshot ea657ce5 to 2ab627a6:
C /restic/cmd_diff.go

View file

@ -163,8 +163,8 @@ example from a local to a remote repository, you can use the ``copy`` command:
.. code-block:: console
$ restic -r /srv/restic-repo-copy copy --from-repo /srv/restic-repo
repository d6504c63 opened successfully, password is correct
repository 3dd0878c opened successfully, password is correct
repository d6504c63 opened successfully
repository 3dd0878c opened successfully
snapshot 410b18a2 of [/home/user/work] at 2020-06-09 23:15:57.305305 +0200 CEST by user@kasimir
copy started, this may take a while...
@ -263,7 +263,7 @@ the unwanted files from affected snapshots by rewriting them using the
.. code-block:: console
$ restic -r /srv/restic-repo rewrite --exclude secret-file
repository c881945a opened (repository version 2) successfully, password is correct
repository c881945a opened (repository version 2) successfully
snapshot 6160ddb2 of [/home/user/work] at 2022-06-12 16:01:28.406630608 +0200 CEST by user@kasimir
excluding /home/user/work/secret-file
@ -274,7 +274,7 @@ the unwanted files from affected snapshots by rewriting them using the
modified 1 snapshots
$ restic -r /srv/restic-repo rewrite --exclude secret-file 6160ddb2
repository c881945a opened (repository version 2) successfully, password is correct
repository c881945a opened (repository version 2) successfully
snapshot 6160ddb2 of [/home/user/work] at 2022-06-12 16:01:28.406630608 +0200 CEST by user@kasimir
excluding /home/user/work/secret-file

View file

@ -80,7 +80,7 @@ command must be run:
$ restic -r /srv/restic-repo prune
enter password for repository:
repository 33002c5e opened successfully, password is correct
repository 33002c5e opened successfully
loading all snapshots...
loading indexes...
finding data that is still in use for 4 snapshots
@ -265,7 +265,7 @@ Sunday for 12 weeks:
.. code-block:: console
$ restic snapshots
repository f00c6e2a opened successfully, password is correct
repository f00c6e2a opened successfully
ID Time Host Tags Paths
---------------------------------------------------------------
0a1f9759 2019-09-01 11:00:00 mopped /home/user/work
@ -289,7 +289,7 @@ four Sundays, and remove the other snapshots:
.. code-block:: console
$ restic forget --keep-daily 4 --dry-run
repository f00c6e2a opened successfully, password is correct
repository f00c6e2a opened successfully
Applying Policy: keep the last 4 daily snapshots
keep 4 snapshots:
ID Time Host Tags Reasons Paths

View file

@ -173,7 +173,8 @@ Summary is the last output line in a successful backup.
+---------------------------+---------------------------------------------------------+
| ``total_duration`` | Total time it took for the operation to complete |
+---------------------------+---------------------------------------------------------+
| ``snapshot_id`` | ID of the new snapshot |
| ``snapshot_id`` | ID of the new snapshot. Field is omitted if snapshot |
| | creation was skipped |
+---------------------------+---------------------------------------------------------+

View file

@ -329,7 +329,6 @@ required to restore the latest snapshot (from any host that made it):
.. code-block:: console
$ restic stats latest
password is correct
Total File Count: 10538
Total Size: 37.824 GiB
@ -340,7 +339,6 @@ host by using the ``--host`` flag:
.. code-block:: console
$ restic stats --host myserver latest
password is correct
Total File Count: 21766
Total Size: 481.783 GiB
@ -357,7 +355,6 @@ has restic's deduplication helped? We can check:
.. code-block:: console
$ restic stats --host myserver --mode raw-data latest
password is correct
Total Blob Count: 340847
Total Size: 458.663 GiB

View file

@ -767,6 +767,8 @@ type SnapshotOptions struct {
Time time.Time
ParentSnapshot *restic.Snapshot
ProgramVersion string
// SkipIfUnchanged omits the snapshot creation if it is identical to the parent snapshot.
SkipIfUnchanged bool
}
// loadParentTree loads a tree referenced by snapshot id. If id is null, nil is returned.
@ -880,6 +882,13 @@ func (arch *Archiver) Snapshot(ctx context.Context, targets []string, opts Snaps
return nil, restic.ID{}, nil, err
}
if opts.ParentSnapshot != nil && opts.SkipIfUnchanged {
ps := opts.ParentSnapshot
if ps.Tree != nil && rootTreeID.Equal(*ps.Tree) {
return nil, restic.ID{}, arch.summary, nil
}
}
sn, err := restic.NewSnapshot(targets, opts.Tags, opts.Hostname, opts.Time)
if err != nil {
return nil, restic.ID{}, nil, err

View file

@ -164,6 +164,11 @@ func (b *JSONProgress) ReportTotal(start time.Time, s archiver.ScanStats) {
// Finish prints the finishing messages.
func (b *JSONProgress) Finish(snapshotID restic.ID, start time.Time, summary *archiver.Summary, dryRun bool) {
id := ""
// empty if snapshot creation was skipped
if !snapshotID.IsNull() {
id = snapshotID.String()
}
b.print(summaryOutput{
MessageType: "summary",
FilesNew: summary.Files.New,
@ -179,7 +184,7 @@ func (b *JSONProgress) Finish(snapshotID restic.ID, start time.Time, summary *ar
TotalFilesProcessed: summary.Files.New + summary.Files.Changed + summary.Files.Unchanged,
TotalBytesProcessed: summary.ProcessedBytes,
TotalDuration: time.Since(start).Seconds(),
SnapshotID: snapshotID.String(),
SnapshotID: id,
DryRun: dryRun,
})
}
@ -235,6 +240,6 @@ type summaryOutput struct {
TotalFilesProcessed uint `json:"total_files_processed"`
TotalBytesProcessed uint64 `json:"total_bytes_processed"`
TotalDuration float64 `json:"total_duration"` // in seconds
SnapshotID string `json:"snapshot_id"`
SnapshotID string `json:"snapshot_id,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}

View file

@ -126,7 +126,7 @@ func (b *TextProgress) Reset() {
}
// Finish prints the finishing messages.
func (b *TextProgress) Finish(_ restic.ID, start time.Time, summary *archiver.Summary, dryRun bool) {
func (b *TextProgress) Finish(id restic.ID, start time.Time, summary *archiver.Summary, dryRun bool) {
b.P("\n")
b.P("Files: %5d new, %5d changed, %5d unmodified\n", summary.Files.New, summary.Files.Changed, summary.Files.Unchanged)
b.P("Dirs: %5d new, %5d changed, %5d unmodified\n", summary.Dirs.New, summary.Dirs.Changed, summary.Dirs.Unchanged)
@ -145,4 +145,12 @@ func (b *TextProgress) Finish(_ restic.ID, start time.Time, summary *archiver.Su
ui.FormatBytes(summary.ProcessedBytes),
ui.FormatDuration(time.Since(start)),
)
if !dryRun {
if id.IsNull() {
b.P("skipped creating snapshot\n")
} else {
b.P("snapshot %s saved\n", id.Str())
}
}
}