backup: implement --skip-if-unchanged

This commit is contained in:
Michael Eischer 2024-05-22 16:38:00 +02:00
parent 7b4f81d964
commit 6869bdaaa8
6 changed files with 52 additions and 14 deletions

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

@ -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

@ -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())
}
}
}