github.com/mckael/restic@v0.8.3/cmd/restic/cmd_tag.go (about)

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