2020-05-05 19:03:57 +00:00
package main
import (
"context"
2022-09-06 20:30:45 +00:00
"fmt"
2020-05-05 19:03:57 +00:00
"github.com/spf13/cobra"
2022-09-06 19:48:20 +00:00
"github.com/restic/restic/internal/backend"
2020-05-05 19:03:57 +00:00
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
2022-09-06 20:30:45 +00:00
"github.com/restic/restic/internal/walker"
2020-05-05 19:03:57 +00:00
)
var cmdRewrite = & cobra . Command {
2022-09-06 20:00:37 +00:00
Use : "rewrite [flags] [all|snapshotID ...]" ,
Short : "Rewrite existing snapshots" ,
2020-05-05 19:03:57 +00:00
Long : `
The "rewrite" command excludes files from existing snapshots .
2022-09-06 20:00:37 +00:00
By default ' rewrite ' will create new snapshots that will contains same data as
the source snapshots but without excluded files . All metadata ( time , host , tags )
will be preserved . The special tag ' rewrite ' will be added to new snapshots to
distinguish it from the source ( unless -- inplace is used ) .
2020-05-05 19:03:57 +00:00
If -- inplace option is used , old snapshot will be removed from repository .
2022-09-06 20:00:37 +00:00
Snapshots to rewrite are specified using -- host , -- tag , -- path or by providing
a list of snapshot ids . Alternatively it ' s possible to use special snapshot id ' all '
that will match all snapshots .
2020-05-05 19:03:57 +00:00
2022-09-06 20:00:37 +00:00
Please note , that this command only creates new snapshots . In order to delete
data from the repository use ' prune ' command .
2020-05-05 19:03:57 +00:00
EXIT STATUS
== == == == == =
Exit status is 0 if the command was successful , and non - zero if there was any error .
` ,
DisableAutoGenTag : true ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-09-06 19:48:20 +00:00
return runRewrite ( cmd . Context ( ) , rewriteOptions , globalOptions , args )
2020-05-05 19:03:57 +00:00
} ,
}
2022-09-06 20:00:37 +00:00
// RewriteOptions collects all options for the rewrite command.
2020-05-05 19:03:57 +00:00
type RewriteOptions struct {
Hosts [ ] string
Paths [ ] string
Tags restic . TagLists
Inplace bool
DryRun bool
// Exclude options
Excludes [ ] string
InsensitiveExcludes [ ] string
ExcludeFiles [ ] string
}
var rewriteOptions RewriteOptions
func init ( ) {
cmdRoot . AddCommand ( cmdRewrite )
f := cmdRewrite . Flags ( )
f . StringArrayVarP ( & rewriteOptions . Hosts , "host" , "H" , nil , "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)" )
f . Var ( & rewriteOptions . Tags , "tag" , "only consider snapshots which include this `taglist`, when no snapshot-ID is given" )
f . StringArrayVar ( & rewriteOptions . Paths , "path" , nil , "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given" )
f . BoolVarP ( & rewriteOptions . Inplace , "inplace" , "" , false , "replace existing snapshots" )
f . BoolVarP ( & rewriteOptions . DryRun , "dry-run" , "n" , false , "do not do anything, just print what would be done" )
// Excludes
f . StringArrayVarP ( & rewriteOptions . Excludes , "exclude" , "e" , nil , "exclude a `pattern` (can be specified multiple times)" )
f . StringArrayVar ( & rewriteOptions . InsensitiveExcludes , "iexclude" , nil , "same as --exclude `pattern` but ignores the casing of filenames" )
f . StringArrayVar ( & rewriteOptions . ExcludeFiles , "exclude-file" , nil , "read exclude patterns from a `file` (can be specified multiple times)" )
}
func collectRejectFuncsForRewrite ( opts RewriteOptions ) ( fs [ ] RejectByNameFunc , err error ) {
//TODO: merge with cmd_backup
// add patterns from file
if len ( opts . ExcludeFiles ) > 0 {
excludes , err := readExcludePatternsFromFiles ( opts . ExcludeFiles )
if err != nil {
return nil , err
}
opts . Excludes = append ( opts . Excludes , excludes ... )
}
if len ( opts . InsensitiveExcludes ) > 0 {
fs = append ( fs , rejectByInsensitivePattern ( opts . InsensitiveExcludes ) )
}
if len ( opts . Excludes ) > 0 {
fs = append ( fs , rejectByPattern ( opts . Excludes ) )
}
return fs , nil
}
func rewriteSnapshot ( ctx context . Context , repo * repository . Repository , sn * restic . Snapshot , opts RewriteOptions , gopts GlobalOptions ) ( bool , error ) {
if sn . Tree == nil {
return false , errors . Errorf ( "snapshot %v has nil tree" , sn . ID ( ) . Str ( ) )
}
rejectByNameFuncs , err := collectRejectFuncsForRewrite ( opts )
if err != nil {
return false , err
}
checkExclude := func ( nodepath string ) bool {
for _ , reject := range rejectByNameFuncs {
if reject ( nodepath ) {
return true
}
}
return false
}
2022-09-06 20:30:45 +00:00
filteredTree , err := walker . FilterTree ( ctx , repo , "/" , * sn . Tree , & walker . TreeFilterVisitor {
CheckExclude : checkExclude ,
PrintExclude : func ( path string ) { Verbosef ( fmt . Sprintf ( "excluding %s\n" , path ) ) } ,
} )
2020-05-05 19:03:57 +00:00
if err != nil {
return false , err
}
if filteredTree == * sn . Tree {
2022-09-06 20:30:45 +00:00
debug . Log ( "Snapshot %v not modified" , sn )
2020-05-05 19:03:57 +00:00
return false , nil
}
2022-09-06 20:30:45 +00:00
debug . Log ( "Snapshot %v modified" , sn )
2020-05-05 19:03:57 +00:00
if opts . DryRun {
2022-09-06 20:30:45 +00:00
Printf ( "Would modify snapshot: %s\n" , sn . String ( ) )
2020-05-05 19:03:57 +00:00
return true , nil
}
err = repo . Flush ( ctx )
if err != nil {
return false , err
}
// Retain the original snapshot id over all tag changes.
if sn . Original == nil {
sn . Original = sn . ID ( )
}
* sn . Tree = filteredTree
if ! opts . Inplace {
sn . AddTags ( [ ] string { "rewrite" } )
}
// Save the new snapshot.
2022-09-06 19:48:20 +00:00
id , err := restic . SaveSnapshot ( ctx , repo , sn )
2020-05-05 19:03:57 +00:00
if err != nil {
return false , err
}
if opts . Inplace {
h := restic . Handle { Type : restic . SnapshotFile , Name : sn . ID ( ) . String ( ) }
if err = repo . Backend ( ) . Remove ( ctx , h ) ; err != nil {
return false , err
}
debug . Log ( "old snapshot %v removed" , sn . ID ( ) )
}
Printf ( "new snapshot saved as %v\n" , id )
return true , nil
}
2022-09-06 19:48:20 +00:00
func runRewrite ( ctx context . Context , opts RewriteOptions , gopts GlobalOptions , args [ ] string ) error {
2020-05-05 19:03:57 +00:00
if len ( opts . Hosts ) == 0 && len ( opts . Tags ) == 0 && len ( opts . Paths ) == 0 && len ( args ) == 0 {
return errors . Fatal ( "no snapshots provided" )
}
if len ( opts . ExcludeFiles ) == 0 && len ( opts . Excludes ) == 0 && len ( opts . InsensitiveExcludes ) == 0 {
return errors . Fatal ( "Nothing to do: no excludes provided" )
}
if len ( args ) == 1 && args [ 0 ] == "all" {
args = [ ] string { }
}
2022-09-06 19:48:20 +00:00
repo , err := OpenRepository ( ctx , gopts )
2020-05-05 19:03:57 +00:00
if err != nil {
return err
}
2022-09-06 20:00:37 +00:00
if ! opts . DryRun {
2020-05-05 19:03:57 +00:00
Verbosef ( "create exclusive lock for repository\n" )
2022-09-06 19:48:20 +00:00
var lock * restic . Lock
lock , ctx , err = lockRepoExclusive ( ctx , repo )
2020-05-05 19:03:57 +00:00
defer unlockRepo ( lock )
if err != nil {
return err
}
2022-09-06 20:22:09 +00:00
} else {
repo . SetDryRun ( )
2020-05-05 19:03:57 +00:00
}
2022-09-06 19:48:20 +00:00
snapshotLister , err := backend . MemorizeList ( ctx , repo . Backend ( ) , restic . SnapshotFile )
if err != nil {
2020-05-05 19:03:57 +00:00
return err
}
2022-09-06 19:48:20 +00:00
if err = repo . LoadIndex ( ctx ) ; err != nil {
return err
}
2020-05-05 19:03:57 +00:00
changedCount := 0
2022-09-06 19:48:20 +00:00
for sn := range FindFilteredSnapshots ( ctx , snapshotLister , repo , opts . Hosts , opts . Tags , opts . Paths , args ) {
2020-05-05 19:03:57 +00:00
Verbosef ( "Checking snapshot %s\n" , sn . String ( ) )
changed , err := rewriteSnapshot ( ctx , repo , sn , opts , gopts )
if err != nil {
Warnf ( "unable to rewrite snapshot ID %q, ignoring: %v\n" , sn . ID ( ) , err )
continue
}
if changed {
changedCount ++
}
}
if changedCount == 0 {
Verbosef ( "no snapshots modified\n" )
} else {
if ! opts . DryRun {
Verbosef ( "modified %v snapshots\n" , changedCount )
} else {
2022-09-06 20:30:45 +00:00
Verbosef ( "dry run. would modify %v snapshots\n" , changedCount )
2020-05-05 19:03:57 +00:00
}
}
return nil
}