github.com/mckael/restic@v0.8.3/cmd/restic/cmd_check.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "os" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/spf13/cobra" 11 12 "github.com/restic/restic/internal/checker" 13 "github.com/restic/restic/internal/errors" 14 "github.com/restic/restic/internal/restic" 15 ) 16 17 var cmdCheck = &cobra.Command{ 18 Use: "check [flags]", 19 Short: "Check the repository for errors", 20 Long: ` 21 The "check" command tests the repository for errors and reports any errors it 22 finds. It can also be used to read all data and therefore simulate a restore. 23 24 By default, the "check" command will always load all data directly from the 25 repository and not use a local cache. 26 `, 27 DisableAutoGenTag: true, 28 RunE: func(cmd *cobra.Command, args []string) error { 29 return runCheck(checkOptions, globalOptions, args) 30 }, 31 PreRunE: func(cmd *cobra.Command, args []string) error { 32 return checkFlags(checkOptions) 33 }, 34 } 35 36 // CheckOptions bundles all options for the 'check' command. 37 type CheckOptions struct { 38 ReadData bool 39 ReadDataSubset string 40 CheckUnused bool 41 WithCache bool 42 } 43 44 var checkOptions CheckOptions 45 46 func init() { 47 cmdRoot.AddCommand(cmdCheck) 48 49 f := cmdCheck.Flags() 50 f.BoolVar(&checkOptions.ReadData, "read-data", false, "read all data blobs") 51 f.StringVar(&checkOptions.ReadDataSubset, "read-data-subset", "", "read subset of data packs") 52 f.BoolVar(&checkOptions.CheckUnused, "check-unused", false, "find unused blobs") 53 f.BoolVar(&checkOptions.WithCache, "with-cache", false, "use the cache") 54 } 55 56 func checkFlags(opts CheckOptions) error { 57 if opts.ReadData && opts.ReadDataSubset != "" { 58 return errors.Fatalf("check flags --read-data and --read-data-subset cannot be used together") 59 } 60 if opts.ReadDataSubset != "" { 61 dataSubset, err := stringToIntSlice(opts.ReadDataSubset) 62 if err != nil || len(dataSubset) != 2 { 63 return errors.Fatalf("check flag --read-data-subset must have two positive integer values, e.g. --read-data-subset=1/2") 64 } 65 if dataSubset[0] == 0 || dataSubset[1] == 0 || dataSubset[0] > dataSubset[1] { 66 return errors.Fatalf("check flag --read-data-subset=n/t values must be positive integers, and n <= t, e.g. --read-data-subset=1/2") 67 } 68 } 69 70 return nil 71 } 72 73 // stringToIntSlice converts string to []uint, using '/' as element separator 74 func stringToIntSlice(param string) (split []uint, err error) { 75 if param == "" { 76 return nil, nil 77 } 78 parts := strings.Split(param, "/") 79 result := make([]uint, len(parts)) 80 for idx, part := range parts { 81 uintval, err := strconv.ParseUint(part, 10, 0) 82 if err != nil { 83 return nil, err 84 } 85 result[idx] = uint(uintval) 86 } 87 return result, nil 88 } 89 90 func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress { 91 if gopts.Quiet { 92 return nil 93 } 94 95 readProgress := restic.NewProgress() 96 97 readProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { 98 status := fmt.Sprintf("[%s] %s %d / %d items", 99 formatDuration(d), 100 formatPercent(s.Blobs, todo.Blobs), 101 s.Blobs, todo.Blobs) 102 103 if w := stdoutTerminalWidth(); w > 0 { 104 if len(status) > w { 105 max := w - len(status) - 4 106 status = status[:max] + "... " 107 } 108 } 109 110 PrintProgress("%s", status) 111 } 112 113 readProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { 114 fmt.Printf("\nduration: %s\n", formatDuration(d)) 115 } 116 117 return readProgress 118 } 119 120 func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error { 121 if len(args) != 0 { 122 return errors.Fatal("check has no arguments") 123 } 124 125 if !opts.WithCache { 126 // do not use a cache for the checker 127 gopts.NoCache = true 128 } 129 130 repo, err := OpenRepository(gopts) 131 if err != nil { 132 return err 133 } 134 135 if !gopts.NoLock { 136 Verbosef("create exclusive lock for repository\n") 137 lock, err := lockRepoExclusive(repo) 138 defer unlockRepo(lock) 139 if err != nil { 140 return err 141 } 142 } 143 144 chkr := checker.New(repo) 145 146 Verbosef("load indexes\n") 147 hints, errs := chkr.LoadIndex(gopts.ctx) 148 149 dupFound := false 150 for _, hint := range hints { 151 Printf("%v\n", hint) 152 if _, ok := hint.(checker.ErrDuplicatePacks); ok { 153 dupFound = true 154 } 155 } 156 157 if dupFound { 158 Printf("\nrun `restic rebuild-index' to correct this\n") 159 } 160 161 if len(errs) > 0 { 162 for _, err := range errs { 163 Warnf("error: %v\n", err) 164 } 165 return errors.Fatal("LoadIndex returned errors") 166 } 167 168 errorsFound := false 169 errChan := make(chan error) 170 171 Verbosef("check all packs\n") 172 go chkr.Packs(gopts.ctx, errChan) 173 174 for err := range errChan { 175 errorsFound = true 176 fmt.Fprintf(os.Stderr, "%v\n", err) 177 } 178 179 Verbosef("check snapshots, trees and blobs\n") 180 errChan = make(chan error) 181 go chkr.Structure(gopts.ctx, errChan) 182 183 for err := range errChan { 184 errorsFound = true 185 if e, ok := err.(checker.TreeError); ok { 186 fmt.Fprintf(os.Stderr, "error for tree %v:\n", e.ID.Str()) 187 for _, treeErr := range e.Errors { 188 fmt.Fprintf(os.Stderr, " %v\n", treeErr) 189 } 190 } else { 191 fmt.Fprintf(os.Stderr, "error: %v\n", err) 192 } 193 } 194 195 if opts.CheckUnused { 196 for _, id := range chkr.UnusedBlobs() { 197 Verbosef("unused blob %v\n", id.Str()) 198 errorsFound = true 199 } 200 } 201 202 doReadData := func(bucket, totalBuckets uint) { 203 packs := restic.IDSet{} 204 for pack := range chkr.GetPacks() { 205 if (uint(pack[0]) % totalBuckets) == (bucket - 1) { 206 packs.Insert(pack) 207 } 208 } 209 packCount := uint64(len(packs)) 210 211 if packCount < chkr.CountPacks() { 212 Verbosef(fmt.Sprintf("read group #%d of %d data packs (out of total %d packs in %d groups)\n", bucket, packCount, chkr.CountPacks(), totalBuckets)) 213 } else { 214 Verbosef("read all data\n") 215 } 216 217 p := newReadProgress(gopts, restic.Stat{Blobs: packCount}) 218 errChan := make(chan error) 219 220 go chkr.ReadPacks(gopts.ctx, packs, p, errChan) 221 222 for err := range errChan { 223 errorsFound = true 224 fmt.Fprintf(os.Stderr, "%v\n", err) 225 } 226 } 227 228 switch { 229 case opts.ReadData: 230 doReadData(1, 1) 231 case opts.ReadDataSubset != "": 232 dataSubset, _ := stringToIntSlice(opts.ReadDataSubset) 233 doReadData(dataSubset[0], dataSubset[1]) 234 } 235 236 if errorsFound { 237 return errors.Fatal("repository contains errors") 238 } 239 240 Verbosef("no errors were found\n") 241 242 return nil 243 }