mirror of
https://github.com/octoleo/restic.git
synced 2024-11-22 21:05:10 +00:00
Add tag
: Manipulate tags on existing snapshots
Add integration testing.
This commit is contained in:
parent
db08581352
commit
f6a258b4a8
189
src/cmds/restic/cmd_tag.go
Normal file
189
src/cmds/restic/cmd_tag.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"restic"
|
||||||
|
"restic/debug"
|
||||||
|
"restic/errors"
|
||||||
|
"restic/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdTag = &cobra.Command{
|
||||||
|
Use: "tag [flags] [snapshot-ID ...]",
|
||||||
|
Short: "modifies tags on snapshots",
|
||||||
|
Long: `
|
||||||
|
The "tag" command allows you to modify tags on exiting snapshots.
|
||||||
|
|
||||||
|
You can either set/replace the entire set of tags on a snapshot, or
|
||||||
|
add tags to/remove tags from the existing set.
|
||||||
|
|
||||||
|
When no snapshot-ID is given, all snapshots matching the host, tag and path filter criteria are modified.
|
||||||
|
`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runTag(tagOptions, globalOptions, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagOptions bundles all options for the 'tag' command.
|
||||||
|
type TagOptions struct {
|
||||||
|
Host string
|
||||||
|
Paths []string
|
||||||
|
Tags []string
|
||||||
|
SetTags []string
|
||||||
|
AddTags []string
|
||||||
|
RemoveTags []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagOptions TagOptions
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdRoot.AddCommand(cmdTag)
|
||||||
|
|
||||||
|
tagFlags := cmdTag.Flags()
|
||||||
|
tagFlags.StringSliceVar(&tagOptions.SetTags, "set", nil, "`tag` which will replace the existing tags (can be given multiple times)")
|
||||||
|
tagFlags.StringSliceVar(&tagOptions.AddTags, "add", nil, "`tag` which will be added to the existing tags (can be given multiple times)")
|
||||||
|
tagFlags.StringSliceVar(&tagOptions.RemoveTags, "remove", nil, "`tag` which will be removed from the existing tags (can be given multiple times)")
|
||||||
|
|
||||||
|
tagFlags.StringVarP(&tagOptions.Host, "host", "H", "", `only consider snapshots for this host, when no snapshot ID is given`)
|
||||||
|
tagFlags.StringSliceVar(&tagOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given")
|
||||||
|
tagFlags.StringSliceVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeTags(repo *repository.Repository, snapshotID restic.ID, setTags, addTags, removeTags, tags, paths []string, host string) (bool, error) {
|
||||||
|
var changed bool
|
||||||
|
|
||||||
|
sn, err := restic.LoadSnapshot(repo, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if (host != "" && host != sn.Hostname) || !sn.HasTags(tags) || !restic.SamePaths(sn.Paths, paths) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(setTags) != 0 {
|
||||||
|
// Setting the tag to an empty string really means no more tags.
|
||||||
|
if len(setTags) == 1 && setTags[0] == "" {
|
||||||
|
setTags = nil
|
||||||
|
}
|
||||||
|
sn.Tags = setTags
|
||||||
|
changed = true
|
||||||
|
} else {
|
||||||
|
for _, add := range addTags {
|
||||||
|
found := false
|
||||||
|
for _, tag := range sn.Tags {
|
||||||
|
if tag == add {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
sn.Tags = append(sn.Tags, add)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, remove := range removeTags {
|
||||||
|
for i, tag := range sn.Tags {
|
||||||
|
if tag == remove {
|
||||||
|
// https://github.com/golang/go/wiki/SliceTricks
|
||||||
|
sn.Tags[i] = sn.Tags[len(sn.Tags)-1]
|
||||||
|
sn.Tags[len(sn.Tags)-1] = ""
|
||||||
|
sn.Tags = sn.Tags[:len(sn.Tags)-1]
|
||||||
|
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
// Save the new snapshot.
|
||||||
|
id, err := repo.SaveJSONUnpacked(restic.SnapshotFile, sn)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log("new snapshot saved as %v", id.Str())
|
||||||
|
|
||||||
|
if err = repo.Flush(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the old snapshot.
|
||||||
|
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
||||||
|
if err = repo.Backend().Remove(h); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log("old snapshot %v removed", sn.ID())
|
||||||
|
}
|
||||||
|
return changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTag(opts TagOptions, gopts GlobalOptions, args []string) error {
|
||||||
|
if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 {
|
||||||
|
return errors.Fatal("nothing to do!")
|
||||||
|
}
|
||||||
|
if len(opts.SetTags) != 0 && (len(opts.AddTags) != 0 || len(opts.RemoveTags) != 0) {
|
||||||
|
return errors.Fatal("--set and --add/--remove cannot be given at the same time")
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := OpenRepository(gopts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !gopts.NoLock {
|
||||||
|
Verbosef("Create exclusive lock for repository\n")
|
||||||
|
lock, err := lockRepoExclusive(repo)
|
||||||
|
defer unlockRepo(lock)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids restic.IDs
|
||||||
|
if len(args) != 0 {
|
||||||
|
// When explit snapshot-IDs are given, the filtering does not matter anymore.
|
||||||
|
opts.Host = ""
|
||||||
|
opts.Tags = nil
|
||||||
|
opts.Paths = nil
|
||||||
|
|
||||||
|
// Process all snapshot IDs given as arguments.
|
||||||
|
for _, s := range args {
|
||||||
|
snapshotID, err := restic.FindSnapshot(repo, s)
|
||||||
|
if err != nil {
|
||||||
|
Warnf("could not find a snapshot for ID %q, ignoring: %v\n", s, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids = append(ids, snapshotID)
|
||||||
|
}
|
||||||
|
ids = ids.Uniq()
|
||||||
|
} else {
|
||||||
|
// If there were no arguments, just get all snapshots.
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
for snapshotID := range repo.List(restic.SnapshotFile, done) {
|
||||||
|
ids = append(ids, snapshotID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCnt := 0
|
||||||
|
for _, id := range ids {
|
||||||
|
changed, err := changeTags(repo, id, opts.SetTags, opts.AddTags, opts.RemoveTags, opts.Tags, opts.Paths, opts.Host)
|
||||||
|
if err != nil {
|
||||||
|
Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
changeCnt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changeCnt == 0 {
|
||||||
|
Verbosef("No snapshots were modified\n")
|
||||||
|
} else {
|
||||||
|
Verbosef("Modified tags on %v snapshots\n", changeCnt)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -655,6 +655,62 @@ func TestBackupTags(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) {
|
||||||
|
OK(t, runTag(opts, gopts, []string{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTag(t *testing.T) {
|
||||||
|
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
|
||||||
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
testRunInit(t, gopts)
|
||||||
|
SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
|
|
||||||
|
testRunBackup(t, []string{env.testdata}, BackupOptions{}, gopts)
|
||||||
|
testRunCheck(t, gopts)
|
||||||
|
newest, _ := testRunSnapshots(t, gopts)
|
||||||
|
Assert(t, newest != nil, "expected a new backup, got nil")
|
||||||
|
Assert(t, len(newest.Tags) == 0,
|
||||||
|
"expected no tags, got %v", newest.Tags)
|
||||||
|
|
||||||
|
testRunTag(t, TagOptions{SetTags: []string{"NL"}}, gopts)
|
||||||
|
testRunCheck(t, gopts)
|
||||||
|
newest, _ = testRunSnapshots(t, gopts)
|
||||||
|
Assert(t, newest != nil, "expected a new backup, got nil")
|
||||||
|
Assert(t, len(newest.Tags) == 1 && newest.Tags[0] == "NL",
|
||||||
|
"set failed, expected one NL tag, got %v", newest.Tags)
|
||||||
|
|
||||||
|
testRunTag(t, TagOptions{AddTags: []string{"CH"}}, gopts)
|
||||||
|
testRunCheck(t, gopts)
|
||||||
|
newest, _ = testRunSnapshots(t, gopts)
|
||||||
|
Assert(t, newest != nil, "expected a new backup, got nil")
|
||||||
|
Assert(t, len(newest.Tags) == 2 && newest.Tags[0] == "NL" && newest.Tags[1] == "CH",
|
||||||
|
"add failed, expected CH,NL tags, got %v", newest.Tags)
|
||||||
|
|
||||||
|
testRunTag(t, TagOptions{RemoveTags: []string{"NL"}}, gopts)
|
||||||
|
testRunCheck(t, gopts)
|
||||||
|
newest, _ = testRunSnapshots(t, gopts)
|
||||||
|
Assert(t, newest != nil, "expected a new backup, got nil")
|
||||||
|
Assert(t, len(newest.Tags) == 1 && newest.Tags[0] == "CH",
|
||||||
|
"remove failed, expected one CH tag, got %v", newest.Tags)
|
||||||
|
|
||||||
|
testRunTag(t, TagOptions{AddTags: []string{"US", "RU"}}, gopts)
|
||||||
|
testRunTag(t, TagOptions{RemoveTags: []string{"CH", "US", "RU"}}, gopts)
|
||||||
|
testRunCheck(t, gopts)
|
||||||
|
newest, _ = testRunSnapshots(t, gopts)
|
||||||
|
Assert(t, newest != nil, "expected a new backup, got nil")
|
||||||
|
Assert(t, len(newest.Tags) == 0,
|
||||||
|
"expected no tags, got %v", newest.Tags)
|
||||||
|
|
||||||
|
// Check special case of removing all tags.
|
||||||
|
testRunTag(t, TagOptions{SetTags: []string{""}}, gopts)
|
||||||
|
testRunCheck(t, gopts)
|
||||||
|
newest, _ = testRunSnapshots(t, gopts)
|
||||||
|
Assert(t, newest != nil, "expected a new backup, got nil")
|
||||||
|
Assert(t, len(newest.Tags) == 0,
|
||||||
|
"expected no tags, got %v", newest.Tags)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
|
func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user