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