1package main 2 3import ( 4 "context" 5 6 "github.com/spf13/cobra" 7 8 "github.com/restic/restic/internal/debug" 9 "github.com/restic/restic/internal/errors" 10 "github.com/restic/restic/internal/repository" 11 "github.com/restic/restic/internal/restic" 12) 13 14var cmdTag = &cobra.Command{ 15 Use: "tag [flags] [snapshot-ID ...]", 16 Short: "Modify tags on snapshots", 17 Long: ` 18The "tag" command allows you to modify tags on exiting snapshots. 19 20You can either set/replace the entire set of tags on a snapshot, or 21add tags to/remove tags from the existing set. 22 23When no snapshot-ID is given, all snapshots matching the host, tag and path filter criteria are modified. 24 25EXIT STATUS 26=========== 27 28Exit status is 0 if the command was successful, and non-zero if there was any error. 29`, 30 DisableAutoGenTag: true, 31 RunE: func(cmd *cobra.Command, args []string) error { 32 return runTag(tagOptions, globalOptions, args) 33 }, 34} 35 36// TagOptions bundles all options for the 'tag' command. 37type TagOptions struct { 38 Hosts []string 39 Paths []string 40 Tags restic.TagLists 41 SetTags restic.TagLists 42 AddTags restic.TagLists 43 RemoveTags restic.TagLists 44} 45 46var tagOptions TagOptions 47 48func init() { 49 cmdRoot.AddCommand(cmdTag) 50 51 tagFlags := cmdTag.Flags() 52 tagFlags.Var(&tagOptions.SetTags, "set", "`tags` which will replace the existing tags in the format `tag[,tag,...]` (can be given multiple times)") 53 tagFlags.Var(&tagOptions.AddTags, "add", "`tags` which will be added to the existing tags in the format `tag[,tag,...]` (can be given multiple times)") 54 tagFlags.Var(&tagOptions.RemoveTags, "remove", "`tags` which will be removed from the existing tags in the format `tag[,tag,...]` (can be given multiple times)") 55 56 tagFlags.StringArrayVarP(&tagOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)") 57 tagFlags.Var(&tagOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given") 58 tagFlags.StringArrayVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") 59} 60 61func changeTags(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, setTags, addTags, removeTags []string) (bool, error) { 62 var changed bool 63 64 if len(setTags) != 0 { 65 // Setting the tag to an empty string really means no tags. 66 if len(setTags) == 1 && setTags[0] == "" { 67 setTags = nil 68 } 69 sn.Tags = setTags 70 changed = true 71 } else { 72 changed = sn.AddTags(addTags) 73 if sn.RemoveTags(removeTags) { 74 changed = true 75 } 76 } 77 78 if changed { 79 // Retain the original snapshot id over all tag changes. 80 if sn.Original == nil { 81 sn.Original = sn.ID() 82 } 83 84 // Save the new snapshot. 85 id, err := repo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn) 86 if err != nil { 87 return false, err 88 } 89 90 debug.Log("new snapshot saved as %v", id) 91 92 if err = repo.Flush(ctx); err != nil { 93 return false, err 94 } 95 96 // Remove the old snapshot. 97 h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()} 98 if err = repo.Backend().Remove(ctx, h); err != nil { 99 return false, err 100 } 101 102 debug.Log("old snapshot %v removed", sn.ID()) 103 } 104 return changed, nil 105} 106 107func runTag(opts TagOptions, gopts GlobalOptions, args []string) error { 108 if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 { 109 return errors.Fatal("nothing to do!") 110 } 111 if len(opts.SetTags) != 0 && (len(opts.AddTags) != 0 || len(opts.RemoveTags) != 0) { 112 return errors.Fatal("--set and --add/--remove cannot be given at the same time") 113 } 114 115 repo, err := OpenRepository(gopts) 116 if err != nil { 117 return err 118 } 119 120 if !gopts.NoLock { 121 Verbosef("create exclusive lock for repository\n") 122 lock, err := lockRepoExclusive(gopts.ctx, repo) 123 defer unlockRepo(lock) 124 if err != nil { 125 return err 126 } 127 } 128 129 changeCnt := 0 130 ctx, cancel := context.WithCancel(gopts.ctx) 131 defer cancel() 132 for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) { 133 changed, err := changeTags(ctx, repo, sn, opts.SetTags.Flatten(), opts.AddTags.Flatten(), opts.RemoveTags.Flatten()) 134 if err != nil { 135 Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err) 136 continue 137 } 138 if changed { 139 changeCnt++ 140 } 141 } 142 if changeCnt == 0 { 143 Verbosef("no snapshots were modified\n") 144 } else { 145 Verbosef("modified tags on %v snapshots\n", changeCnt) 146 } 147 return nil 148} 149