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  }