2020-05-05 22:03:57 +03:00
package main
import (
"context"
2022-09-06 22:30:45 +02:00
"fmt"
2020-05-05 22:03:57 +03:00
"github.com/spf13/cobra"
2022-09-06 21:48:20 +02:00
"github.com/restic/restic/internal/backend"
2020-05-05 22:03:57 +03: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 22:30:45 +02:00
"github.com/restic/restic/internal/walker"
2020-05-05 22:03:57 +03:00
)
var cmdRewrite = & cobra . Command {
2022-09-06 22:37:56 +02:00
Use : "rewrite [flags] [snapshotID ...]" ,
2022-09-06 22:00:37 +02:00
Short : "Rewrite existing snapshots" ,
2020-05-05 22:03:57 +03:00
Long : `
The "rewrite" command excludes files from existing snapshots .
2022-09-06 22:00:37 +02: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 22:03:57 +03:00
If -- inplace option is used , old snapshot will be removed from repository .
2022-09-06 22:00:37 +02:00
Snapshots to rewrite are specified using -- host , -- tag , -- path or by providing
2022-09-06 22:37:56 +02:00
a list of snapshot ids . Not specifying a snapshot id will rewrite all snapshots .
2020-05-05 22:03:57 +03:00
2022-09-06 22:00:37 +02:00
Please note , that this command only creates new snapshots . In order to delete
data from the repository use ' prune ' command .
2020-05-05 22:03:57 +03: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 21:48:20 +02:00
return runRewrite ( cmd . Context ( ) , rewriteOptions , globalOptions , args )
2020-05-05 22:03:57 +03:00
} ,
}
2022-09-06 22:00:37 +02:00
// RewriteOptions collects all options for the rewrite command.
2020-05-05 22:03:57 +03:00
type RewriteOptions struct {
Hosts [ ] string
Paths [ ] string
Tags restic . TagLists
Inplace bool
DryRun bool
// Exclude options
2022-09-06 22:56:29 +02:00
Excludes [ ] string
InsensitiveExcludes [ ] string
ExcludeFiles [ ] string
InsensitiveExcludeFiles [ ] string
2020-05-05 22:03:57 +03:00
}
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)" )
2022-09-06 22:56:29 +02:00
f . StringArrayVar ( & rewriteOptions . InsensitiveExcludeFiles , "iexclude-file" , nil , "same as --exclude-file but ignores casing of `file`names in patterns" )
2020-05-05 22:03:57 +03:00
}
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 ( ) )
}
2022-09-06 22:56:29 +02:00
rejectByNameFuncs , err := collectExcludePatterns ( opts . Excludes , opts . InsensitiveExcludes , opts . ExcludeFiles , opts . InsensitiveExcludeFiles )
2020-05-05 22:03:57 +03:00
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 22:30:45 +02: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 22:03:57 +03:00
if err != nil {
return false , err
}
if filteredTree == * sn . Tree {
2022-09-06 22:30:45 +02:00
debug . Log ( "Snapshot %v not modified" , sn )
2020-05-05 22:03:57 +03:00
return false , nil
}
2022-09-06 22:30:45 +02:00
debug . Log ( "Snapshot %v modified" , sn )
2020-05-05 22:03:57 +03:00
if opts . DryRun {
2022-09-06 22:30:45 +02:00
Printf ( "Would modify snapshot: %s\n" , sn . String ( ) )
2020-05-05 22:03:57 +03: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 21:48:20 +02:00
id , err := restic . SaveSnapshot ( ctx , repo , sn )
2020-05-05 22:03:57 +03: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 21:48:20 +02:00
func runRewrite ( ctx context . Context , opts RewriteOptions , gopts GlobalOptions , args [ ] string ) error {
2020-05-05 22:03:57 +03:00
if len ( opts . ExcludeFiles ) == 0 && len ( opts . Excludes ) == 0 && len ( opts . InsensitiveExcludes ) == 0 {
return errors . Fatal ( "Nothing to do: no excludes provided" )
}
2022-09-06 21:48:20 +02:00
repo , err := OpenRepository ( ctx , gopts )
2020-05-05 22:03:57 +03:00
if err != nil {
return err
}
2022-09-06 22:00:37 +02:00
if ! opts . DryRun {
2020-05-05 22:03:57 +03:00
Verbosef ( "create exclusive lock for repository\n" )
2022-09-06 21:48:20 +02:00
var lock * restic . Lock
lock , ctx , err = lockRepoExclusive ( ctx , repo )
2020-05-05 22:03:57 +03:00
defer unlockRepo ( lock )
if err != nil {
return err
}
2022-09-06 22:22:09 +02:00
} else {
repo . SetDryRun ( )
2020-05-05 22:03:57 +03:00
}
2022-09-06 21:48:20 +02:00
snapshotLister , err := backend . MemorizeList ( ctx , repo . Backend ( ) , restic . SnapshotFile )
if err != nil {
2020-05-05 22:03:57 +03:00
return err
}
2022-09-06 21:48:20 +02:00
if err = repo . LoadIndex ( ctx ) ; err != nil {
return err
}
2020-05-05 22:03:57 +03:00
changedCount := 0
2022-09-06 21:48:20 +02:00
for sn := range FindFilteredSnapshots ( ctx , snapshotLister , repo , opts . Hosts , opts . Tags , opts . Paths , args ) {
2020-05-05 22:03:57 +03: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 22:30:45 +02:00
Verbosef ( "dry run. would modify %v snapshots\n" , changedCount )
2020-05-05 22:03:57 +03:00
}
}
return nil
}