mirror of https://github.com/restic/restic.git
Issue: 4942: cmd_rewrite: add snapshot summary data to an existing snapshot. (#5185)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
This commit is contained in:
parent
060a44202f
commit
4104a8e6a5
|
@ -0,0 +1,11 @@
|
||||||
|
Enhancement: support creating snapshot summary statistics for old snapshots
|
||||||
|
|
||||||
|
When `rewrite` is used with the `--snapshot-summary` option, a new snapshot is
|
||||||
|
created containing statistics summary data. Only two fields in the summary will
|
||||||
|
be non-zero: `TotalFilesProcessed` and `TotalBytesProcessed`.
|
||||||
|
|
||||||
|
When rewrite is called with one of the `--exclude` options, `TotalFilesProcessed`
|
||||||
|
and `TotalBytesProcessed` will be updated in the snapshot summary.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4942
|
||||||
|
https://github.com/restic/restic/pull/5185
|
|
@ -143,8 +143,9 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt
|
||||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) {
|
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) {
|
||||||
Verbosef("\n%v\n", sn)
|
Verbosef("\n%v\n", sn)
|
||||||
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
|
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
|
||||||
func(ctx context.Context, sn *restic.Snapshot) (restic.ID, error) {
|
func(ctx context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error) {
|
||||||
return rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
|
id, err := rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
|
||||||
|
return id, nil, err
|
||||||
}, opts.DryRun, opts.Forget, nil, "repaired")
|
}, opts.DryRun, opts.Forget, nil, "repaired")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err)
|
return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err)
|
||||||
|
|
|
@ -35,6 +35,13 @@ Please note that the --forget option only removes the snapshots and not the actu
|
||||||
data stored in the repository. In order to delete the no longer referenced data,
|
data stored in the repository. In order to delete the no longer referenced data,
|
||||||
use the "prune" command.
|
use the "prune" command.
|
||||||
|
|
||||||
|
When rewrite is used with the --snapshot-summary option, a new snapshot is
|
||||||
|
created containing statistics summary data. Only two fields in the summary will
|
||||||
|
be non-zero: TotalFilesProcessed and TotalBytesProcessed.
|
||||||
|
|
||||||
|
When rewrite is called with one of the --exclude options, TotalFilesProcessed
|
||||||
|
and TotalBytesProcessed will be updated in the snapshot summary.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
@ -85,6 +92,7 @@ func (sma snapshotMetadataArgs) convert() (*snapshotMetadata, error) {
|
||||||
type RewriteOptions struct {
|
type RewriteOptions struct {
|
||||||
Forget bool
|
Forget bool
|
||||||
DryRun bool
|
DryRun bool
|
||||||
|
SnapshotSummary bool
|
||||||
|
|
||||||
Metadata snapshotMetadataArgs
|
Metadata snapshotMetadataArgs
|
||||||
restic.SnapshotFilter
|
restic.SnapshotFilter
|
||||||
|
@ -101,12 +109,15 @@ func init() {
|
||||||
f.BoolVarP(&rewriteOptions.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done")
|
f.BoolVarP(&rewriteOptions.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done")
|
||||||
f.StringVar(&rewriteOptions.Metadata.Hostname, "new-host", "", "replace hostname")
|
f.StringVar(&rewriteOptions.Metadata.Hostname, "new-host", "", "replace hostname")
|
||||||
f.StringVar(&rewriteOptions.Metadata.Time, "new-time", "", "replace time of the backup")
|
f.StringVar(&rewriteOptions.Metadata.Time, "new-time", "", "replace time of the backup")
|
||||||
|
f.BoolVarP(&rewriteOptions.SnapshotSummary, "snapshot-summary", "s", false, "create snapshot summary record if it does not exist")
|
||||||
|
|
||||||
initMultiSnapshotFilter(f, &rewriteOptions.SnapshotFilter, true)
|
initMultiSnapshotFilter(f, &rewriteOptions.SnapshotFilter, true)
|
||||||
rewriteOptions.ExcludePatternOptions.Add(f)
|
rewriteOptions.ExcludePatternOptions.Add(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
type rewriteFilterFunc func(ctx context.Context, sn *restic.Snapshot) (restic.ID, error)
|
// rewriteFilterFunc returns the filtered tree ID or an error. If a snapshot summary is returned, the snapshot will
|
||||||
|
// be updated accordingly.
|
||||||
|
type rewriteFilterFunc func(ctx context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error)
|
||||||
|
|
||||||
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, opts RewriteOptions) (bool, error) {
|
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, opts RewriteOptions) (bool, error) {
|
||||||
if sn.Tree == nil {
|
if sn.Tree == nil {
|
||||||
|
@ -126,7 +137,7 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *resti
|
||||||
|
|
||||||
var filter rewriteFilterFunc
|
var filter rewriteFilterFunc
|
||||||
|
|
||||||
if len(rejectByNameFuncs) > 0 {
|
if len(rejectByNameFuncs) > 0 || opts.SnapshotSummary {
|
||||||
selectByName := func(nodepath string) bool {
|
selectByName := func(nodepath string) bool {
|
||||||
for _, reject := range rejectByNameFuncs {
|
for _, reject := range rejectByNameFuncs {
|
||||||
if reject(nodepath) {
|
if reject(nodepath) {
|
||||||
|
@ -146,22 +157,24 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *resti
|
||||||
|
|
||||||
rewriter, querySize := walker.NewSnapshotSizeRewriter(rewriteNode)
|
rewriter, querySize := walker.NewSnapshotSizeRewriter(rewriteNode)
|
||||||
|
|
||||||
filter = func(ctx context.Context, sn *restic.Snapshot) (restic.ID, error) {
|
filter = func(ctx context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error) {
|
||||||
id, err := rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
|
id, err := rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return restic.ID{}, err
|
return restic.ID{}, nil, err
|
||||||
}
|
}
|
||||||
ss := querySize()
|
ss := querySize()
|
||||||
|
summary := &restic.SnapshotSummary{}
|
||||||
if sn.Summary != nil {
|
if sn.Summary != nil {
|
||||||
sn.Summary.TotalFilesProcessed = ss.FileCount
|
*summary = *sn.Summary
|
||||||
sn.Summary.TotalBytesProcessed = ss.FileSize
|
|
||||||
}
|
}
|
||||||
return id, err
|
summary.TotalFilesProcessed = ss.FileCount
|
||||||
|
summary.TotalBytesProcessed = ss.FileSize
|
||||||
|
return id, summary, err
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
filter = func(_ context.Context, sn *restic.Snapshot) (restic.ID, error) {
|
filter = func(_ context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error) {
|
||||||
return *sn.Tree, nil
|
return *sn.Tree, nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,9 +189,10 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r
|
||||||
repo.StartPackUploader(wgCtx, wg)
|
repo.StartPackUploader(wgCtx, wg)
|
||||||
|
|
||||||
var filteredTree restic.ID
|
var filteredTree restic.ID
|
||||||
|
var summary *restic.SnapshotSummary
|
||||||
wg.Go(func() error {
|
wg.Go(func() error {
|
||||||
var err error
|
var err error
|
||||||
filteredTree, err = filter(ctx, sn)
|
filteredTree, summary, err = filter(ctx, sn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -203,7 +217,12 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if filteredTree == *sn.Tree && newMetadata == nil {
|
matchingSummary := true
|
||||||
|
if summary != nil {
|
||||||
|
matchingSummary = sn.Summary != nil && *summary == *sn.Summary
|
||||||
|
}
|
||||||
|
|
||||||
|
if filteredTree == *sn.Tree && newMetadata == nil && matchingSummary {
|
||||||
debug.Log("Snapshot %v not modified", sn)
|
debug.Log("Snapshot %v not modified", sn)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -230,6 +249,9 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r
|
||||||
// Always set the original snapshot id as this essentially a new snapshot.
|
// Always set the original snapshot id as this essentially a new snapshot.
|
||||||
sn.Original = sn.ID()
|
sn.Original = sn.ID()
|
||||||
sn.Tree = &filteredTree
|
sn.Tree = &filteredTree
|
||||||
|
if summary != nil {
|
||||||
|
sn.Summary = summary
|
||||||
|
}
|
||||||
|
|
||||||
if !forget {
|
if !forget {
|
||||||
sn.AddTags([]string{addTag})
|
sn.AddTags([]string{addTag})
|
||||||
|
@ -263,7 +285,7 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, args []string) error {
|
func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, args []string) error {
|
||||||
if opts.ExcludePatternOptions.Empty() && opts.Metadata.empty() {
|
if !opts.SnapshotSummary && opts.ExcludePatternOptions.Empty() && opts.Metadata.empty() {
|
||||||
return errors.Fatal("Nothing to do: no excludes provided and no new metadata provided")
|
return errors.Fatal("Nothing to do: no excludes provided and no new metadata provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,3 +139,36 @@ func TestRewriteMetadata(t *testing.T) {
|
||||||
testRewriteMetadata(t, metadata)
|
testRewriteMetadata(t, metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRewriteSnaphotSummary(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
createBasicRewriteRepo(t, env)
|
||||||
|
|
||||||
|
rtest.OK(t, runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, env.gopts, []string{}))
|
||||||
|
// no new snapshot should be created as the snapshot already has a summary
|
||||||
|
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
|
// replace snapshot by one without a summary
|
||||||
|
_, repo, unlock, err := openWithExclusiveLock(context.TODO(), env.gopts, false)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
sn, err := restic.LoadSnapshot(context.TODO(), repo, snapshots[0])
|
||||||
|
rtest.OK(t, err)
|
||||||
|
oldSummary := sn.Summary
|
||||||
|
sn.Summary = nil
|
||||||
|
rtest.OK(t, repo.RemoveUnpacked(context.TODO(), restic.WriteableSnapshotFile, snapshots[0]))
|
||||||
|
snapshots[0], err = restic.SaveSnapshot(context.TODO(), repo, sn)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
unlock()
|
||||||
|
|
||||||
|
// rewrite snapshot and lookup ID of new snapshot
|
||||||
|
rtest.OK(t, runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, env.gopts, []string{}))
|
||||||
|
newSnapshots := testListSnapshots(t, env.gopts, 2)
|
||||||
|
newSnapshot := restic.NewIDSet(newSnapshots...).Sub(restic.NewIDSet(snapshots...)).List()[0]
|
||||||
|
|
||||||
|
sn, err = restic.LoadSnapshot(context.TODO(), repo, newSnapshot)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
rtest.Assert(t, sn.Summary != nil, "snapshot should have summary attached")
|
||||||
|
rtest.Equals(t, oldSummary.TotalBytesProcessed, sn.Summary.TotalBytesProcessed, "unexpected TotalBytesProcessed value")
|
||||||
|
rtest.Equals(t, oldSummary.TotalFilesProcessed, sn.Summary.TotalFilesProcessed, "unexpected TotalFilesProcessed value")
|
||||||
|
}
|
||||||
|
|
|
@ -332,6 +332,10 @@ command, see :ref:`backup-excluding-files` for details.
|
||||||
It is possible to rewrite only a subset of snapshots by filtering them the same
|
It is possible to rewrite only a subset of snapshots by filtering them the same
|
||||||
way as for the ``copy`` command, see :ref:`copy-filtering-snapshots`.
|
way as for the ``copy`` command, see :ref:`copy-filtering-snapshots`.
|
||||||
|
|
||||||
|
The option ``--snapshot-summary`` can be used to attach summary data to existing
|
||||||
|
snapshots that do not have this information. When a snapshot summary is created
|
||||||
|
the only fields added are ``TotalFilesProcessed`` and ``TotalBytesProcessed``.
|
||||||
|
|
||||||
By default, the ``rewrite`` command will keep the original snapshots and create
|
By default, the ``rewrite`` command will keep the original snapshots and create
|
||||||
new ones for every snapshot which was modified during rewriting. The new
|
new ones for every snapshot which was modified during rewriting. The new
|
||||||
snapshots are marked with the tag ``rewrite`` to differentiate them from the
|
snapshots are marked with the tag ``rewrite`` to differentiate them from the
|
||||||
|
|
Loading…
Reference in New Issue