forked from TrueCloudLab/restic
Merge pull request #4816 from MichaelEischer/skip-if-unchanged
backup: add support for `--skip-if-unchanged`
This commit is contained in:
commit
7e0ee5974f
11 changed files with 122 additions and 42 deletions
11
changelog/unreleased/issue-662
Normal file
11
changelog/unreleased/issue-662
Normal 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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
||||
+---------------------------+---------------------------------------------------------+
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue