2015-06-28 22:22:25 +00:00
package main
import (
2018-03-31 08:23:55 +00:00
"io/ioutil"
2020-10-24 15:30:42 +00:00
"math/rand"
2018-01-02 05:38:14 +00:00
"strconv"
"strings"
2021-01-05 15:36:41 +00:00
"time"
2015-06-28 22:22:25 +00:00
2016-09-17 10:36:05 +00:00
"github.com/spf13/cobra"
2017-07-23 12:21:03 +00:00
"github.com/restic/restic/internal/checker"
"github.com/restic/restic/internal/errors"
2018-03-31 08:23:55 +00:00
"github.com/restic/restic/internal/fs"
2017-07-24 15:42:25 +00:00
"github.com/restic/restic/internal/restic"
2015-06-28 22:22:25 +00:00
)
2016-09-17 10:36:05 +00:00
var cmdCheck = & cobra . Command {
Use : "check [flags]" ,
2017-09-11 16:32:44 +00:00
Short : "Check the repository for errors" ,
2016-09-17 10:36:05 +00:00
Long : `
The "check" command tests the repository for errors and reports any errors it
finds . It can also be used to read all data and therefore simulate a restore .
2017-07-18 20:15:18 +00:00
By default , the "check" command will always load all data directly from the
repository and not use a local cache .
2019-11-05 06:03:38 +00:00
EXIT STATUS
== == == == == =
Exit status is 0 if the command was successful , and non - zero if there was any error .
2016-09-17 10:36:05 +00:00
` ,
2017-08-06 19:02:16 +00:00
DisableAutoGenTag : true ,
2016-09-17 10:36:05 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
return runCheck ( checkOptions , globalOptions , args )
} ,
2018-01-02 05:38:14 +00:00
PreRunE : func ( cmd * cobra . Command , args [ ] string ) error {
return checkFlags ( checkOptions )
} ,
2016-09-17 10:36:05 +00:00
}
2015-06-28 22:22:25 +00:00
2017-03-08 19:09:24 +00:00
// CheckOptions bundles all options for the 'check' command.
2016-09-17 10:36:05 +00:00
type CheckOptions struct {
2018-01-02 05:38:14 +00:00
ReadData bool
ReadDataSubset string
CheckUnused bool
WithCache bool
2015-06-28 22:22:25 +00:00
}
2016-09-17 10:36:05 +00:00
var checkOptions CheckOptions
2015-06-28 22:22:25 +00:00
func init ( ) {
2016-09-17 10:36:05 +00:00
cmdRoot . AddCommand ( cmdCheck )
2015-06-28 22:22:25 +00:00
2016-09-17 10:36:05 +00:00
f := cmdCheck . Flags ( )
2017-02-13 15:02:47 +00:00
f . BoolVar ( & checkOptions . ReadData , "read-data" , false , "read all data blobs" )
2020-10-24 15:30:42 +00:00
f . StringVar ( & checkOptions . ReadDataSubset , "read-data-subset" , "" , "read a `subset` of data packs, specified as 'n/t' for specific subset or either 'x%' or 'x.y%' for random subset" )
2017-02-13 15:02:47 +00:00
f . BoolVar ( & checkOptions . CheckUnused , "check-unused" , false , "find unused blobs" )
2017-07-18 20:15:18 +00:00
f . BoolVar ( & checkOptions . WithCache , "with-cache" , false , "use the cache" )
2015-06-28 22:22:25 +00:00
}
2018-01-02 05:38:14 +00:00
func checkFlags ( opts CheckOptions ) error {
if opts . ReadData && opts . ReadDataSubset != "" {
2020-10-24 15:30:42 +00:00
return errors . Fatal ( "check flags --read-data and --read-data-subset cannot be used together" )
2018-01-02 05:38:14 +00:00
}
if opts . ReadDataSubset != "" {
dataSubset , err := stringToIntSlice ( opts . ReadDataSubset )
2020-10-24 15:30:42 +00:00
argumentError := errors . Fatal ( "check flag --read-data-subset must have two positive integer values or a percentage, e.g. --read-data-subset=1/2 or --read-data-subset=2.5%%" )
if err == nil {
if len ( dataSubset ) != 2 {
return argumentError
}
if dataSubset [ 0 ] == 0 || dataSubset [ 1 ] == 0 || dataSubset [ 0 ] > dataSubset [ 1 ] {
return errors . Fatal ( "check flag --read-data-subset=n/t values must be positive integers, and n <= t, e.g. --read-data-subset=1/2" )
}
if dataSubset [ 1 ] > totalBucketsMax {
return errors . Fatalf ( "check flag --read-data-subset=n/t t must be at most %d" , totalBucketsMax )
}
} else {
percentage , err := parsePercentage ( opts . ReadDataSubset )
if err != nil {
return argumentError
}
if percentage <= 0.0 || percentage > 100.0 {
return errors . Fatal (
2020-11-15 17:12:36 +00:00
"check flag --read-data-subset=n% n must be above 0.0% and at most 100.0%" )
2020-10-24 15:30:42 +00:00
}
2019-06-29 18:34:54 +00:00
}
2018-01-02 05:38:14 +00:00
}
return nil
}
2019-06-29 18:34:54 +00:00
// See doReadData in runCheck below for why this is 256.
const totalBucketsMax = 256
2018-01-02 05:38:14 +00:00
// stringToIntSlice converts string to []uint, using '/' as element separator
func stringToIntSlice ( param string ) ( split [ ] uint , err error ) {
if param == "" {
return nil , nil
}
parts := strings . Split ( param , "/" )
result := make ( [ ] uint , len ( parts ) )
for idx , part := range parts {
uintval , err := strconv . ParseUint ( part , 10 , 0 )
if err != nil {
return nil , err
}
result [ idx ] = uint ( uintval )
}
return result , nil
}
2020-10-24 15:30:42 +00:00
// ParsePercentage parses a percentage string of the form "X%" where X is a float constant,
// and returns the value of that constant. It does not check the range of the value.
func parsePercentage ( s string ) ( float64 , error ) {
if ! strings . HasSuffix ( s , "%" ) {
return 0 , errors . Errorf ( ` parsePercentage: %q does not end in "%%" ` , s )
}
s = s [ : len ( s ) - 1 ]
p , err := strconv . ParseFloat ( s , 64 )
if err != nil {
return 0 , errors . Errorf ( "parsePercentage: %v" , err )
}
return p , nil
}
2018-03-31 08:23:55 +00:00
// prepareCheckCache configures a special cache directory for check.
//
// * if --with-cache is specified, the default cache is used
2018-05-11 14:55:12 +00:00
// * if the user explicitly requested --no-cache, we don't use any cache
2018-07-22 17:24:11 +00:00
// * if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check
2018-03-31 08:23:55 +00:00
// * by default, we use a cache in a temporary directory that is deleted after the check
func prepareCheckCache ( opts CheckOptions , gopts * GlobalOptions ) ( cleanup func ( ) ) {
cleanup = func ( ) { }
if opts . WithCache {
// use the default cache, no setup needed
return cleanup
}
if gopts . NoCache {
// don't use any cache, no setup needed
return cleanup
}
2018-07-22 17:24:11 +00:00
cachedir := gopts . CacheDir
2018-03-31 08:23:55 +00:00
// use a cache in a temporary directory
2018-07-22 17:24:11 +00:00
tempdir , err := ioutil . TempDir ( cachedir , "restic-check-cache-" )
2018-03-31 08:23:55 +00:00
if err != nil {
// if an error occurs, don't use any cache
Warnf ( "unable to create temporary directory for cache during check, disabling cache: %v\n" , err )
gopts . NoCache = true
return cleanup
}
gopts . CacheDir = tempdir
Verbosef ( "using temporary cache in %v\n" , tempdir )
cleanup = func ( ) {
err := fs . RemoveAll ( tempdir )
if err != nil {
Warnf ( "error removing temporary cache directory: %v\n" , err )
}
}
return cleanup
}
2016-09-17 10:36:05 +00:00
func runCheck ( opts CheckOptions , gopts GlobalOptions , args [ ] string ) error {
2015-06-28 22:22:25 +00:00
if len ( args ) != 0 {
2020-10-05 22:08:59 +00:00
return errors . Fatal ( "the check command expects no arguments, only options - please see `restic help check` for usage and flags" )
2015-06-28 22:22:25 +00:00
}
2018-03-31 08:23:55 +00:00
cleanup := prepareCheckCache ( opts , & gopts )
2018-04-01 16:09:53 +00:00
AddCleanupHandler ( func ( ) error {
cleanup ( )
return nil
} )
2017-07-18 20:15:18 +00:00
2016-09-17 10:36:05 +00:00
repo , err := OpenRepository ( gopts )
2015-06-28 22:22:25 +00:00
if err != nil {
return err
}
2016-09-17 10:36:05 +00:00
if ! gopts . NoLock {
2017-10-27 19:06:34 +00:00
Verbosef ( "create exclusive lock for repository\n" )
2020-08-09 11:24:47 +00:00
lock , err := lockRepoExclusive ( gopts . ctx , repo )
2015-11-10 20:41:22 +00:00
defer unlockRepo ( lock )
if err != nil {
return err
}
2015-06-28 22:22:25 +00:00
}
2020-11-06 23:07:32 +00:00
chkr := checker . New ( repo , opts . CheckUnused )
2015-06-28 22:22:25 +00:00
2017-10-27 19:06:34 +00:00
Verbosef ( "load indexes\n" )
use global context for check, debug, dump, find, forget, init, key,
list, mount, tag, unlock commands
gh-1434
2017-12-06 12:02:55 +00:00
hints , errs := chkr . LoadIndex ( gopts . ctx )
2015-10-25 15:26:50 +00:00
2015-10-25 16:24:52 +00:00
dupFound := false
2015-10-25 15:26:50 +00:00
for _ , hint := range hints {
2016-09-17 10:36:05 +00:00
Printf ( "%v\n" , hint )
2015-10-25 16:24:52 +00:00
if _ , ok := hint . ( checker . ErrDuplicatePacks ) ; ok {
dupFound = true
}
}
if dupFound {
2018-04-07 08:07:54 +00:00
Printf ( "This is non-critical, you can run `restic rebuild-index' to correct this\n" )
2015-10-25 15:26:50 +00:00
}
if len ( errs ) > 0 {
for _ , err := range errs {
2016-09-17 10:36:05 +00:00
Warnf ( "error: %v\n" , err )
2015-10-25 15:26:50 +00:00
}
2016-09-01 20:17:37 +00:00
return errors . Fatal ( "LoadIndex returned errors" )
2015-06-28 22:22:25 +00:00
}
2015-07-11 14:00:49 +00:00
errorsFound := false
2018-04-07 08:07:54 +00:00
orphanedPacks := 0
2015-07-11 23:44:19 +00:00
errChan := make ( chan error )
2017-10-27 19:06:34 +00:00
Verbosef ( "check all packs\n" )
use global context for check, debug, dump, find, forget, init, key,
list, mount, tag, unlock commands
gh-1434
2017-12-06 12:02:55 +00:00
go chkr . Packs ( gopts . ctx , errChan )
2015-07-11 23:44:19 +00:00
for err := range errChan {
2018-04-07 08:07:54 +00:00
if checker . IsOrphanedPack ( err ) {
orphanedPacks ++
Verbosef ( "%v\n" , err )
continue
}
2015-07-11 14:00:49 +00:00
errorsFound = true
2020-04-04 17:41:24 +00:00
Warnf ( "%v\n" , err )
2015-07-11 14:00:49 +00:00
}
2018-04-07 08:07:54 +00:00
if orphanedPacks > 0 {
Verbosef ( "%d additional files were found in the repo, which likely contain duplicate data.\nYou can run `restic prune` to correct this.\n" , orphanedPacks )
}
2017-10-27 19:06:34 +00:00
Verbosef ( "check snapshots, trees and blobs\n" )
2015-07-11 23:44:19 +00:00
errChan = make ( chan error )
2020-12-05 23:07:45 +00:00
go func ( ) {
bar := newProgressMax ( ! gopts . Quiet , 0 , "snapshots" )
defer bar . Done ( )
chkr . Structure ( gopts . ctx , bar , errChan )
} ( )
2015-07-11 23:44:19 +00:00
for err := range errChan {
2015-07-11 14:00:49 +00:00
errorsFound = true
2015-10-11 17:13:45 +00:00
if e , ok := err . ( checker . TreeError ) ; ok {
2020-04-04 17:41:24 +00:00
Warnf ( "error for tree %v:\n" , e . ID . Str ( ) )
2015-10-11 17:13:45 +00:00
for _ , treeErr := range e . Errors {
2020-04-04 17:41:24 +00:00
Warnf ( " %v\n" , treeErr )
2015-10-11 17:13:45 +00:00
}
} else {
2020-04-04 17:41:24 +00:00
Warnf ( "error: %v\n" , err )
2015-10-11 17:13:45 +00:00
}
2015-07-11 14:00:49 +00:00
}
2016-09-17 10:36:05 +00:00
if opts . CheckUnused {
2020-11-06 22:41:04 +00:00
for _ , id := range chkr . UnusedBlobs ( gopts . ctx ) {
2020-04-18 17:46:33 +00:00
Verbosef ( "unused blob %v\n" , id )
2015-11-08 19:46:52 +00:00
errorsFound = true
}
2015-07-12 15:09:48 +00:00
}
2020-10-24 15:30:42 +00:00
doReadData := func ( packs map [ restic . ID ] int64 ) {
2018-01-02 05:38:14 +00:00
packCount := uint64 ( len ( packs ) )
2015-12-06 16:09:06 +00:00
2020-08-02 10:22:06 +00:00
p := newProgressMax ( ! gopts . Quiet , packCount , "packs" )
2015-12-06 16:09:06 +00:00
errChan := make ( chan error )
2018-01-02 05:38:14 +00:00
go chkr . ReadPacks ( gopts . ctx , packs , p , errChan )
2015-12-06 16:09:06 +00:00
for err := range errChan {
errorsFound = true
2020-04-04 17:41:24 +00:00
Warnf ( "%v\n" , err )
2015-12-06 16:09:06 +00:00
}
2020-11-08 20:03:59 +00:00
p . Done ( )
2015-12-06 16:09:06 +00:00
}
2018-01-02 05:38:14 +00:00
switch {
case opts . ReadData :
2020-10-24 15:30:42 +00:00
Verbosef ( "read all data\n" )
doReadData ( selectPacksByBucket ( chkr . GetPacks ( ) , 1 , 1 ) )
2018-01-02 05:38:14 +00:00
case opts . ReadDataSubset != "" :
2020-10-24 15:30:42 +00:00
var packs map [ restic . ID ] int64
dataSubset , err := stringToIntSlice ( opts . ReadDataSubset )
if err == nil {
bucket := dataSubset [ 0 ]
totalBuckets := dataSubset [ 1 ]
packs = selectPacksByBucket ( chkr . GetPacks ( ) , bucket , totalBuckets )
packCount := uint64 ( len ( packs ) )
Verbosef ( "read group #%d of %d data packs (out of total %d packs in %d groups)\n" , bucket , packCount , chkr . CountPacks ( ) , totalBuckets )
} else {
percentage , _ := parsePercentage ( opts . ReadDataSubset )
packs = selectRandomPacksByPercentage ( chkr . GetPacks ( ) , percentage )
Verbosef ( "read %.1f%% of data packs\n" , percentage )
}
if packs == nil {
return errors . Fatal ( "internal error: failed to select packs to check" )
}
doReadData ( packs )
2018-01-02 05:38:14 +00:00
}
2015-07-11 14:00:49 +00:00
if errorsFound {
2016-09-01 20:17:37 +00:00
return errors . Fatal ( "repository contains errors" )
2015-07-11 14:00:49 +00:00
}
2017-10-03 06:29:19 +00:00
2017-10-27 19:06:34 +00:00
Verbosef ( "no errors were found\n" )
2017-10-03 06:29:19 +00:00
2015-06-28 22:22:25 +00:00
return nil
}
2020-10-24 15:30:42 +00:00
// selectPacksByBucket selects subsets of packs by ranges of buckets.
func selectPacksByBucket ( allPacks map [ restic . ID ] int64 , bucket , totalBuckets uint ) map [ restic . ID ] int64 {
packs := make ( map [ restic . ID ] int64 )
for pack , size := range allPacks {
// If we ever check more than the first byte
// of pack, update totalBucketsMax.
if ( uint ( pack [ 0 ] ) % totalBuckets ) == ( bucket - 1 ) {
packs [ pack ] = size
}
}
return packs
}
// selectRandomPacksByPercentage selects the given percentage of packs which are randomly choosen.
func selectRandomPacksByPercentage ( allPacks map [ restic . ID ] int64 , percentage float64 ) map [ restic . ID ] int64 {
packCount := len ( allPacks )
packsToCheck := int ( float64 ( packCount ) * ( percentage / 100.0 ) )
2021-02-27 14:56:40 +00:00
if packCount > 0 && packsToCheck < 1 {
2020-10-24 15:30:42 +00:00
packsToCheck = 1
}
2021-01-05 15:36:41 +00:00
timeNs := time . Now ( ) . UnixNano ( )
r := rand . New ( rand . NewSource ( timeNs ) )
idx := r . Perm ( packCount )
2020-10-24 15:30:42 +00:00
var keys [ ] restic . ID
for k := range allPacks {
keys = append ( keys , k )
}
packs := make ( map [ restic . ID ] int64 )
for i := 0 ; i < packsToCheck ; i ++ {
id := keys [ idx [ i ] ]
packs [ id ] = allPacks [ id ]
}
return packs
}