mirror of
https://github.com/restic/restic.git
synced 2026-05-28 04:35:41 -04:00
Merge a01938bdf3 into f000da3b35
This commit is contained in:
commit
29fbbf0f14
4 changed files with 186 additions and 5 deletions
8
changelog/unreleased/issue-5723
Normal file
8
changelog/unreleased/issue-5723
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Enhancement: Allow rewriting snapshot paths with `rewrite --path-to`
|
||||
|
||||
The `rewrite` command now supports `--path-from` and `--path-to` options
|
||||
to replace paths stored in a snapshot. When both options are used, only
|
||||
the matching path is replaced. When only `--path-to` is specified, all
|
||||
paths are replaced with the new path.
|
||||
|
||||
https://github.com/restic/restic/issues/5723
|
||||
|
|
@ -31,6 +31,10 @@ exclude or include filters to control which files are included in the new
|
|||
snapshots. Unless --new-host or --new-time is specified, metadata (time, host,
|
||||
tags) is preserved.
|
||||
|
||||
The --path-to option replaces the stored paths of the snapshot. When used
|
||||
together with --path-from, only the matching path is replaced. When used
|
||||
alone, all paths are replaced with the new path.
|
||||
|
||||
The snapshots to rewrite are specified using the --host, --tag and --path options,
|
||||
or by providing a list of snapshot IDs. Please note that specifying neither any of
|
||||
these options nor a snapshot ID will cause the command to rewrite all snapshots.
|
||||
|
|
@ -74,15 +78,19 @@ Exit status is 12 if the password is incorrect.
|
|||
type snapshotMetadata struct {
|
||||
Hostname string
|
||||
Time *time.Time
|
||||
PathFrom string
|
||||
PathTo string
|
||||
}
|
||||
|
||||
type snapshotMetadataArgs struct {
|
||||
Hostname string
|
||||
Time string
|
||||
PathFrom string
|
||||
PathTo string
|
||||
}
|
||||
|
||||
func (sma snapshotMetadataArgs) empty() bool {
|
||||
return sma.Hostname == "" && sma.Time == ""
|
||||
return sma.Hostname == "" && sma.Time == "" && sma.PathTo == ""
|
||||
}
|
||||
|
||||
func (sma snapshotMetadataArgs) convert() (*snapshotMetadata, error) {
|
||||
|
|
@ -98,7 +106,7 @@ func (sma snapshotMetadataArgs) convert() (*snapshotMetadata, error) {
|
|||
}
|
||||
timeStamp = &t
|
||||
}
|
||||
return &snapshotMetadata{Hostname: sma.Hostname, Time: timeStamp}, nil
|
||||
return &snapshotMetadata{Hostname: sma.Hostname, Time: timeStamp, PathFrom: sma.PathFrom, PathTo: sma.PathTo}, nil
|
||||
}
|
||||
|
||||
// RewriteOptions collects all options for the rewrite command.
|
||||
|
|
@ -118,6 +126,8 @@ func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) {
|
|||
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done")
|
||||
f.StringVar(&opts.Metadata.Hostname, "new-host", "", "replace hostname")
|
||||
f.StringVar(&opts.Metadata.Time, "new-time", "", "replace time of the backup")
|
||||
f.StringVar(&opts.Metadata.PathFrom, "path-from", "", "path to replace in the snapshot (used with --path-to, default: all paths)")
|
||||
f.StringVar(&opts.Metadata.PathTo, "path-to", "", "new path to set in the snapshot")
|
||||
f.BoolVarP(&opts.SnapshotSummary, "snapshot-summary", "s", false, "create snapshot summary record if it does not exist")
|
||||
|
||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||
|
|
@ -248,6 +258,14 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *d
|
|||
printer.P("would set hostname to %s", newMetadata.Hostname)
|
||||
}
|
||||
|
||||
if newMetadata != nil && newMetadata.PathTo != "" {
|
||||
if newMetadata.PathFrom != "" {
|
||||
printer.P("would replace path %q with %q", newMetadata.PathFrom, newMetadata.PathTo)
|
||||
} else {
|
||||
printer.P("would set all paths to %q", newMetadata.PathTo)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
@ -272,6 +290,27 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *d
|
|||
sn.Hostname = newMetadata.Hostname
|
||||
}
|
||||
|
||||
if newMetadata != nil && newMetadata.PathTo != "" {
|
||||
if newMetadata.PathFrom != "" {
|
||||
replaced := false
|
||||
for i, p := range sn.Paths {
|
||||
if p == newMetadata.PathFrom {
|
||||
sn.Paths[i] = newMetadata.PathTo
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if replaced {
|
||||
printer.P("replacing path %q with %q", newMetadata.PathFrom, newMetadata.PathTo)
|
||||
} else {
|
||||
printer.P("path %q not found in snapshot, skipping path replacement", newMetadata.PathFrom)
|
||||
}
|
||||
} else {
|
||||
printer.P("setting all paths to %q", newMetadata.PathTo)
|
||||
sn.Paths = []string{newMetadata.PathTo}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the new snapshot.
|
||||
id, err := data.SaveSnapshot(ctx, repo, sn)
|
||||
if err != nil {
|
||||
|
|
@ -292,7 +331,9 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *d
|
|||
func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
hasExcludes := !opts.ExcludePatternOptions.Empty()
|
||||
hasIncludes := !opts.IncludePatternOptions.Empty()
|
||||
if !opts.SnapshotSummary && !hasExcludes && !hasIncludes && opts.Metadata.empty() {
|
||||
if opts.Metadata.PathFrom != "" && opts.Metadata.PathTo == "" {
|
||||
return errors.Fatal("--path-from requires --path-to")
|
||||
} else if !opts.SnapshotSummary && !hasExcludes && !hasIncludes && opts.Metadata.empty() {
|
||||
return errors.Fatal("Nothing to do: no excludes/includes provided and no new metadata provided")
|
||||
} else if hasExcludes && hasIncludes {
|
||||
return errors.Fatal("exclude and include patterns are mutually exclusive")
|
||||
|
|
|
|||
|
|
@ -330,6 +330,117 @@ func TestRewriteIncludeEmptyDirectory(t *testing.T) {
|
|||
testLsOutputContainsCount(t, env.gopts, LsOptions{}, []string{"latest"}, "empty-directory", 1)
|
||||
}
|
||||
|
||||
func TestRewritePathTo(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
createBasicRewriteRepo(t, env)
|
||||
|
||||
// --path-to without --path-from replaces all paths
|
||||
err := testRunRewriteWithOpts(t,
|
||||
RewriteOptions{
|
||||
Forget: true,
|
||||
Metadata: snapshotMetadataArgs{PathTo: "/new/path"},
|
||||
},
|
||||
env.gopts,
|
||||
[]string{"latest"})
|
||||
rtest.OK(t, err)
|
||||
|
||||
newSnapshots := testListSnapshots(t, env.gopts, 1)
|
||||
sn := getSnapshot(t, newSnapshots[0], env)
|
||||
rtest.Assert(t, len(sn.Paths) == 1, "expected one path, got %v", sn.Paths)
|
||||
rtest.Equals(t, "/new/path", sn.Paths[0])
|
||||
}
|
||||
|
||||
func TestRewritePathFromTo(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
createBasicRewriteRepo(t, env)
|
||||
|
||||
// get the original path from the snapshot
|
||||
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||
origSn := getSnapshot(t, snapshots[0], env)
|
||||
origPath := origSn.Paths[0]
|
||||
|
||||
// --path-from with --path-to replaces only the matching path
|
||||
err := testRunRewriteWithOpts(t,
|
||||
RewriteOptions{
|
||||
Forget: true,
|
||||
Metadata: snapshotMetadataArgs{PathFrom: origPath, PathTo: "/replaced/path"},
|
||||
},
|
||||
env.gopts,
|
||||
[]string{"latest"})
|
||||
rtest.OK(t, err)
|
||||
|
||||
newSnapshots := testListSnapshots(t, env.gopts, 1)
|
||||
sn := getSnapshot(t, newSnapshots[0], env)
|
||||
rtest.Assert(t, len(sn.Paths) == 1, "expected one path, got %v", sn.Paths)
|
||||
rtest.Equals(t, "/replaced/path", sn.Paths[0])
|
||||
}
|
||||
|
||||
func TestRewritePathFromNoMatch(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
createBasicRewriteRepo(t, env)
|
||||
|
||||
// --path-from that doesn't match any path still creates a new snapshot
|
||||
// (metadata was requested to change) but paths stay the same
|
||||
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||
origSn := getSnapshot(t, snapshots[0], env)
|
||||
|
||||
err := testRunRewriteWithOpts(t,
|
||||
RewriteOptions{
|
||||
Forget: true,
|
||||
Metadata: snapshotMetadataArgs{PathFrom: "/nonexistent/path", PathTo: "/new/path"},
|
||||
},
|
||||
env.gopts,
|
||||
[]string{"latest"})
|
||||
rtest.OK(t, err)
|
||||
|
||||
newSnapshots := testListSnapshots(t, env.gopts, 1)
|
||||
newSn := getSnapshot(t, newSnapshots[0], env)
|
||||
rtest.Equals(t, origSn.Paths, newSn.Paths)
|
||||
}
|
||||
|
||||
func TestRewritePathFromWithoutPathTo(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
// --path-from without --path-to should fail
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runRewrite(ctx,
|
||||
RewriteOptions{
|
||||
Metadata: snapshotMetadataArgs{PathFrom: "/some/path"},
|
||||
},
|
||||
gopts, []string{"latest"}, env.gopts.Term)
|
||||
})
|
||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "--path-from requires --path-to"),
|
||||
`expected error containing "--path-from requires --path-to", got: %v`, err)
|
||||
}
|
||||
|
||||
func TestRewritePathToWithExclude(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
createBasicRewriteRepo(t, env)
|
||||
|
||||
err := testRunRewriteWithOpts(t,
|
||||
RewriteOptions{
|
||||
Forget: true,
|
||||
Metadata: snapshotMetadataArgs{
|
||||
PathTo: "/rewritten/path",
|
||||
},
|
||||
ExcludePatternOptions: filter.ExcludePatternOptions{Excludes: []string{"*.txt"}},
|
||||
},
|
||||
env.gopts,
|
||||
[]string{"latest"})
|
||||
rtest.OK(t, err)
|
||||
|
||||
newSnapshots := testListSnapshots(t, env.gopts, 1)
|
||||
sn := getSnapshot(t, newSnapshots[0], env)
|
||||
rtest.Equals(t, "/rewritten/path", sn.Paths[0])
|
||||
testLsOutputContainsCount(t, env.gopts, LsOptions{}, []string{"latest"}, ".txt", 0)
|
||||
}
|
||||
|
||||
// TestRewriteIncludeNothing makes sure when nothing is included, the original snapshot stays untouched
|
||||
func TestRewriteIncludeNothing(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
|
|
|
|||
|
|
@ -387,8 +387,17 @@ Modifying metadata of snapshots
|
|||
===============================
|
||||
|
||||
Sometimes it may be desirable to change the metadata of an existing snapshot.
|
||||
Currently, rewriting the hostname and the time of the backup is supported.
|
||||
This is possible using the ``rewrite`` command with the option ``--new-host`` followed by the desired new hostname or the option ``--new-time`` followed by the desired new timestamp.
|
||||
Currently, rewriting the hostname, the time of the backup, and the stored
|
||||
paths is supported.
|
||||
|
||||
This is possible using the ``rewrite`` command with the option ``--new-host``
|
||||
followed by the desired new hostname, the option ``--new-time`` followed by
|
||||
the desired new timestamp, or the option ``--path-to`` to replace the stored
|
||||
paths.
|
||||
|
||||
When ``--path-to`` is used alone, all paths in the snapshot are replaced with
|
||||
the new path. When combined with ``--path-from``, only the matching path is
|
||||
replaced, which is useful for snapshots that contain multiple paths.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
|
|
@ -404,6 +413,18 @@ This is possible using the ``rewrite`` command with the option ``--new-host`` fo
|
|||
|
||||
modified 1 snapshots
|
||||
|
||||
To replace the stored path of a snapshot:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic rewrite --path-to /new/path --forget latest
|
||||
|
||||
To replace only a specific path in a multi-path snapshot:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic rewrite --path-from /old/path --path-to /new/path --forget latest
|
||||
|
||||
|
||||
.. _checking-integrity:
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue