github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/cmd/restic/global.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strings"
    12  	"syscall"
    13  	"time"
    14  
    15  	"github.com/restic/restic/internal/backend"
    16  	"github.com/restic/restic/internal/backend/azure"
    17  	"github.com/restic/restic/internal/backend/b2"
    18  	"github.com/restic/restic/internal/backend/gs"
    19  	"github.com/restic/restic/internal/backend/local"
    20  	"github.com/restic/restic/internal/backend/location"
    21  	"github.com/restic/restic/internal/backend/rest"
    22  	"github.com/restic/restic/internal/backend/s3"
    23  	"github.com/restic/restic/internal/backend/sftp"
    24  	"github.com/restic/restic/internal/backend/swift"
    25  	"github.com/restic/restic/internal/cache"
    26  	"github.com/restic/restic/internal/debug"
    27  	"github.com/restic/restic/internal/fs"
    28  	"github.com/restic/restic/internal/limiter"
    29  	"github.com/restic/restic/internal/options"
    30  	"github.com/restic/restic/internal/repository"
    31  	"github.com/restic/restic/internal/restic"
    32  
    33  	"github.com/restic/restic/internal/errors"
    34  
    35  	"golang.org/x/crypto/ssh/terminal"
    36  )
    37  
    38  var version = "compiled manually"
    39  
    40  // GlobalOptions hold all global options for restic.
    41  type GlobalOptions struct {
    42  	Repo         string
    43  	PasswordFile string
    44  	Quiet        bool
    45  	NoLock       bool
    46  	JSON         bool
    47  	CacheDir     string
    48  	NoCache      bool
    49  	CACerts      []string
    50  	CleanupCache bool
    51  
    52  	LimitUploadKb   int
    53  	LimitDownloadKb int
    54  
    55  	ctx      context.Context
    56  	password string
    57  	stdout   io.Writer
    58  	stderr   io.Writer
    59  
    60  	Options []string
    61  
    62  	extended options.Options
    63  }
    64  
    65  var globalOptions = GlobalOptions{
    66  	stdout: os.Stdout,
    67  	stderr: os.Stderr,
    68  }
    69  
    70  func init() {
    71  	var cancel context.CancelFunc
    72  	globalOptions.ctx, cancel = context.WithCancel(context.Background())
    73  	AddCleanupHandler(func() error {
    74  		cancel()
    75  		return nil
    76  	})
    77  
    78  	f := cmdRoot.PersistentFlags()
    79  	f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
    80  	f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
    81  	f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
    82  	f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
    83  	f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
    84  	f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory")
    85  	f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
    86  	f.StringSliceVar(&globalOptions.CACerts, "cacert", nil, "path to load root certificates from (default: use system certificates)")
    87  	f.BoolVar(&globalOptions.CleanupCache, "cleanup-cache", false, "auto remove old cache directories")
    88  	f.IntVar(&globalOptions.LimitUploadKb, "limit-upload", 0, "limits uploads to a maximum rate in KiB/s. (default: unlimited)")
    89  	f.IntVar(&globalOptions.LimitDownloadKb, "limit-download", 0, "limits downloads to a maximum rate in KiB/s. (default: unlimited)")
    90  	f.StringSliceVarP(&globalOptions.Options, "option", "o", []string{}, "set extended option (`key=value`, can be specified multiple times)")
    91  
    92  	restoreTerminal()
    93  }
    94  
    95  // checkErrno returns nil when err is set to syscall.Errno(0), since this is no
    96  // error condition.
    97  func checkErrno(err error) error {
    98  	e, ok := err.(syscall.Errno)
    99  	if !ok {
   100  		return err
   101  	}
   102  
   103  	if e == 0 {
   104  		return nil
   105  	}
   106  
   107  	return err
   108  }
   109  
   110  func stdinIsTerminal() bool {
   111  	return terminal.IsTerminal(int(os.Stdin.Fd()))
   112  }
   113  
   114  func stdoutIsTerminal() bool {
   115  	return terminal.IsTerminal(int(os.Stdout.Fd()))
   116  }
   117  
   118  func stdoutTerminalWidth() int {
   119  	w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
   120  	if err != nil {
   121  		return 0
   122  	}
   123  	return w
   124  }
   125  
   126  // restoreTerminal installs a cleanup handler that restores the previous
   127  // terminal state on exit.
   128  func restoreTerminal() {
   129  	if !stdoutIsTerminal() {
   130  		return
   131  	}
   132  
   133  	fd := int(os.Stdout.Fd())
   134  	state, err := terminal.GetState(fd)
   135  	if err != nil {
   136  		fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err)
   137  		return
   138  	}
   139  
   140  	AddCleanupHandler(func() error {
   141  		err := checkErrno(terminal.Restore(fd, state))
   142  		if err != nil {
   143  			fmt.Fprintf(os.Stderr, "unable to get restore terminal state: %#+v\n", err)
   144  		}
   145  		return err
   146  	})
   147  }
   148  
   149  // ClearLine creates a platform dependent string to clear the current
   150  // line, so it can be overwritten. ANSI sequences are not supported on
   151  // current windows cmd shell.
   152  func ClearLine() string {
   153  	if runtime.GOOS == "windows" {
   154  		if w := stdoutTerminalWidth(); w > 0 {
   155  			return strings.Repeat(" ", w-1) + "\r"
   156  		}
   157  		return ""
   158  	}
   159  	return "\x1b[2K"
   160  }
   161  
   162  // Printf writes the message to the configured stdout stream.
   163  func Printf(format string, args ...interface{}) {
   164  	_, err := fmt.Fprintf(globalOptions.stdout, format, args...)
   165  	if err != nil {
   166  		fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
   167  		Exit(100)
   168  	}
   169  }
   170  
   171  // Verbosef calls Printf to write the message when the verbose flag is set.
   172  func Verbosef(format string, args ...interface{}) {
   173  	if globalOptions.Quiet {
   174  		return
   175  	}
   176  
   177  	Printf(format, args...)
   178  }
   179  
   180  // PrintProgress wraps fmt.Printf to handle the difference in writing progress
   181  // information to terminals and non-terminal stdout
   182  func PrintProgress(format string, args ...interface{}) {
   183  	var (
   184  		message         string
   185  		carriageControl string
   186  	)
   187  	message = fmt.Sprintf(format, args...)
   188  
   189  	if !(strings.HasSuffix(message, "\r") || strings.HasSuffix(message, "\n")) {
   190  		if stdoutIsTerminal() {
   191  			carriageControl = "\r"
   192  		} else {
   193  			carriageControl = "\n"
   194  		}
   195  		message = fmt.Sprintf("%s%s", message, carriageControl)
   196  	}
   197  
   198  	if stdoutIsTerminal() {
   199  		message = fmt.Sprintf("%s%s", ClearLine(), message)
   200  	}
   201  
   202  	fmt.Print(message)
   203  }
   204  
   205  // Warnf writes the message to the configured stderr stream.
   206  func Warnf(format string, args ...interface{}) {
   207  	_, err := fmt.Fprintf(globalOptions.stderr, format, args...)
   208  	if err != nil {
   209  		fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err)
   210  		Exit(100)
   211  	}
   212  }
   213  
   214  // Exitf uses Warnf to write the message and then terminates the process with
   215  // the given exit code.
   216  func Exitf(exitcode int, format string, args ...interface{}) {
   217  	if format[len(format)-1] != '\n' {
   218  		format += "\n"
   219  	}
   220  
   221  	Warnf(format, args...)
   222  	Exit(exitcode)
   223  }
   224  
   225  // resolvePassword determines the password to be used for opening the repository.
   226  func resolvePassword(opts GlobalOptions, env string) (string, error) {
   227  	if opts.PasswordFile != "" {
   228  		s, err := ioutil.ReadFile(opts.PasswordFile)
   229  		if os.IsNotExist(err) {
   230  			return "", errors.Fatalf("%s does not exist", opts.PasswordFile)
   231  		}
   232  		return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
   233  	}
   234  
   235  	if pwd := os.Getenv(env); pwd != "" {
   236  		return pwd, nil
   237  	}
   238  
   239  	return "", nil
   240  }
   241  
   242  // readPassword reads the password from the given reader directly.
   243  func readPassword(in io.Reader) (password string, err error) {
   244  	buf := make([]byte, 1000)
   245  	n, err := io.ReadFull(in, buf)
   246  	buf = buf[:n]
   247  
   248  	if err != nil && errors.Cause(err) != io.ErrUnexpectedEOF {
   249  		return "", errors.Wrap(err, "ReadFull")
   250  	}
   251  
   252  	return strings.TrimRight(string(buf), "\r\n"), nil
   253  }
   254  
   255  // readPasswordTerminal reads the password from the given reader which must be a
   256  // tty. Prompt is printed on the writer out before attempting to read the
   257  // password.
   258  func readPasswordTerminal(in *os.File, out io.Writer, prompt string) (password string, err error) {
   259  	fmt.Fprint(out, prompt)
   260  	buf, err := terminal.ReadPassword(int(in.Fd()))
   261  	fmt.Fprintln(out)
   262  	if err != nil {
   263  		return "", errors.Wrap(err, "ReadPassword")
   264  	}
   265  
   266  	password = string(buf)
   267  	return password, nil
   268  }
   269  
   270  // ReadPassword reads the password from a password file, the environment
   271  // variable RESTIC_PASSWORD or prompts the user.
   272  func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
   273  	if opts.password != "" {
   274  		return opts.password, nil
   275  	}
   276  
   277  	var (
   278  		password string
   279  		err      error
   280  	)
   281  
   282  	if stdinIsTerminal() {
   283  		password, err = readPasswordTerminal(os.Stdin, os.Stderr, prompt)
   284  	} else {
   285  		password, err = readPassword(os.Stdin)
   286  	}
   287  
   288  	if err != nil {
   289  		return "", errors.Wrap(err, "unable to read password")
   290  	}
   291  
   292  	if len(password) == 0 {
   293  		return "", errors.Fatal("an empty password is not a password")
   294  	}
   295  
   296  	return password, nil
   297  }
   298  
   299  // ReadPasswordTwice calls ReadPassword two times and returns an error when the
   300  // passwords don't match.
   301  func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, error) {
   302  	pw1, err := ReadPassword(gopts, prompt1)
   303  	if err != nil {
   304  		return "", err
   305  	}
   306  	pw2, err := ReadPassword(gopts, prompt2)
   307  	if err != nil {
   308  		return "", err
   309  	}
   310  
   311  	if pw1 != pw2 {
   312  		return "", errors.Fatal("passwords do not match")
   313  	}
   314  
   315  	return pw1, nil
   316  }
   317  
   318  const maxKeys = 20
   319  
   320  // OpenRepository reads the password and opens the repository.
   321  func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
   322  	if opts.Repo == "" {
   323  		return nil, errors.Fatal("Please specify repository location (-r)")
   324  	}
   325  
   326  	be, err := open(opts.Repo, opts.extended)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	if opts.LimitUploadKb > 0 || opts.LimitDownloadKb > 0 {
   332  		debug.Log("rate limiting backend to %d KiB/s upload and %d KiB/s download", opts.LimitUploadKb, opts.LimitDownloadKb)
   333  		be = limiter.LimitBackend(be, limiter.NewStaticLimiter(opts.LimitUploadKb, opts.LimitDownloadKb))
   334  	}
   335  
   336  	be = backend.NewRetryBackend(be, 10, func(msg string, err error, d time.Duration) {
   337  		Warnf("%v returned error, retrying after %v: %v\n", msg, d, err)
   338  	})
   339  
   340  	s := repository.New(be)
   341  
   342  	opts.password, err = ReadPassword(opts, "enter password for repository: ")
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  
   347  	err = s.SearchKey(opts.ctx, opts.password, maxKeys)
   348  	if err != nil {
   349  		return nil, err
   350  	}
   351  
   352  	if stdoutIsTerminal() {
   353  		Verbosef("password is correct\n")
   354  	}
   355  
   356  	if opts.NoCache {
   357  		return s, nil
   358  	}
   359  
   360  	c, err := cache.New(s.Config().ID, opts.CacheDir)
   361  	if err != nil {
   362  		Warnf("unable to open cache: %v\n", err)
   363  		return s, nil
   364  	}
   365  
   366  	// start using the cache
   367  	s.UseCache(c)
   368  
   369  	oldCacheDirs, err := cache.Old(c.Base)
   370  	if err != nil {
   371  		Warnf("unable to find old cache directories: %v", err)
   372  	}
   373  
   374  	// nothing more to do if no old cache dirs could be found
   375  	if len(oldCacheDirs) == 0 {
   376  		return s, nil
   377  	}
   378  
   379  	// cleanup old cache dirs if instructed to do so
   380  	if opts.CleanupCache {
   381  		Printf("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base)
   382  
   383  		for _, item := range oldCacheDirs {
   384  			dir := filepath.Join(c.Base, item)
   385  			err = fs.RemoveAll(dir)
   386  			if err != nil {
   387  				Warnf("unable to remove %v: %v\n", dir, err)
   388  			}
   389  		}
   390  	} else {
   391  		Verbosef("found %d old cache directories in %v, pass --cleanup-cache to remove them\n",
   392  			len(oldCacheDirs), c.Base)
   393  	}
   394  
   395  	return s, nil
   396  }
   397  
   398  func parseConfig(loc location.Location, opts options.Options) (interface{}, error) {
   399  	// only apply options for a particular backend here
   400  	opts = opts.Extract(loc.Scheme)
   401  
   402  	switch loc.Scheme {
   403  	case "local":
   404  		cfg := loc.Config.(local.Config)
   405  		if err := opts.Apply(loc.Scheme, &cfg); err != nil {
   406  			return nil, err
   407  		}
   408  
   409  		debug.Log("opening local repository at %#v", cfg)
   410  		return cfg, nil
   411  
   412  	case "sftp":
   413  		cfg := loc.Config.(sftp.Config)
   414  		if err := opts.Apply(loc.Scheme, &cfg); err != nil {
   415  			return nil, err
   416  		}
   417  
   418  		debug.Log("opening sftp repository at %#v", cfg)
   419  		return cfg, nil
   420  
   421  	case "s3":
   422  		cfg := loc.Config.(s3.Config)
   423  		if cfg.KeyID == "" {
   424  			cfg.KeyID = os.Getenv("AWS_ACCESS_KEY_ID")
   425  		}
   426  
   427  		if cfg.Secret == "" {
   428  			cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
   429  		}
   430  
   431  		if err := opts.Apply(loc.Scheme, &cfg); err != nil {
   432  			return nil, err
   433  		}
   434  
   435  		debug.Log("opening s3 repository at %#v", cfg)
   436  		return cfg, nil
   437  
   438  	case "gs":
   439  		cfg := loc.Config.(gs.Config)
   440  		if cfg.ProjectID == "" {
   441  			cfg.ProjectID = os.Getenv("GOOGLE_PROJECT_ID")
   442  		}
   443  
   444  		if cfg.JSONKeyPath == "" {
   445  			if path := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"); path != "" {
   446  				// Check read access
   447  				if _, err := ioutil.ReadFile(path); err != nil {
   448  					return nil, errors.Fatalf("Failed to read google credential from file %v: %v", path, err)
   449  				}
   450  				cfg.JSONKeyPath = path
   451  			} else {
   452  				return nil, errors.Fatal("No credential file path is set")
   453  			}
   454  		}
   455  
   456  		if err := opts.Apply(loc.Scheme, &cfg); err != nil {
   457  			return nil, err
   458  		}
   459  
   460  		debug.Log("opening gs repository at %#v", cfg)
   461  		return cfg, nil
   462  
   463  	case "azure":
   464  		cfg := loc.Config.(azure.Config)
   465  		if cfg.AccountName == "" {
   466  			cfg.AccountName = os.Getenv("AZURE_ACCOUNT_NAME")
   467  		}
   468  
   469  		if cfg.AccountKey == "" {
   470  			cfg.AccountKey = os.Getenv("AZURE_ACCOUNT_KEY")
   471  		}
   472  
   473  		if err := opts.Apply(loc.Scheme, &cfg); err != nil {
   474  			return nil, err
   475  		}
   476  
   477  		debug.Log("opening gs repository at %#v", cfg)
   478  		return cfg, nil
   479  
   480  	case "swift":
   481  		cfg := loc.Config.(swift.Config)
   482  
   483  		if err := swift.ApplyEnvironment("", &cfg); err != nil {
   484  			return nil, err
   485  		}
   486  
   487  		if err := opts.Apply(loc.Scheme, &cfg); err != nil {
   488  			return nil, err
   489  		}
   490  
   491  		debug.Log("opening swift repository at %#v", cfg)
   492  		return cfg, nil
   493  
   494  	case "b2":
   495  		cfg := loc.Config.(b2.Config)
   496  
   497  		if cfg.AccountID == "" {
   498  			cfg.AccountID = os.Getenv("B2_ACCOUNT_ID")
   499  		}
   500  
   501  		if cfg.Key == "" {
   502  			cfg.Key = os.Getenv("B2_ACCOUNT_KEY")
   503  		}
   504  
   505  		if err := opts.Apply(loc.Scheme, &cfg); err != nil {
   506  			return nil, err
   507  		}
   508  
   509  		debug.Log("opening b2 repository at %#v", cfg)
   510  		return cfg, nil
   511  	case "rest":
   512  		cfg := loc.Config.(rest.Config)
   513  		if err := opts.Apply(loc.Scheme, &cfg); err != nil {
   514  			return nil, err
   515  		}
   516  
   517  		debug.Log("opening rest repository at %#v", cfg)
   518  		return cfg, nil
   519  	}
   520  
   521  	return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
   522  }
   523  
   524  // Open the backend specified by a location config.
   525  func open(s string, opts options.Options) (restic.Backend, error) {
   526  	debug.Log("parsing location %v", s)
   527  	loc, err := location.Parse(s)
   528  	if err != nil {
   529  		return nil, errors.Fatalf("parsing repository location failed: %v", err)
   530  	}
   531  
   532  	var be restic.Backend
   533  
   534  	cfg, err := parseConfig(loc, opts)
   535  	if err != nil {
   536  		return nil, err
   537  	}
   538  
   539  	rt, err := backend.Transport(globalOptions.CACerts)
   540  	if err != nil {
   541  		return nil, err
   542  	}
   543  
   544  	switch loc.Scheme {
   545  	case "local":
   546  		be, err = local.Open(cfg.(local.Config))
   547  	case "sftp":
   548  		be, err = sftp.Open(cfg.(sftp.Config), SuspendSignalHandler, InstallSignalHandler)
   549  	case "s3":
   550  		be, err = s3.Open(cfg.(s3.Config), rt)
   551  	case "gs":
   552  		be, err = gs.Open(cfg.(gs.Config))
   553  	case "azure":
   554  		be, err = azure.Open(cfg.(azure.Config), rt)
   555  	case "swift":
   556  		be, err = swift.Open(cfg.(swift.Config), rt)
   557  	case "b2":
   558  		be, err = b2.Open(globalOptions.ctx, cfg.(b2.Config), rt)
   559  	case "rest":
   560  		be, err = rest.Open(cfg.(rest.Config), rt)
   561  
   562  	default:
   563  		return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
   564  	}
   565  
   566  	if err != nil {
   567  		return nil, errors.Fatalf("unable to open repo at %v: %v", s, err)
   568  	}
   569  
   570  	// check if config is there
   571  	fi, err := be.Stat(globalOptions.ctx, restic.Handle{Type: restic.ConfigFile})
   572  	if err != nil {
   573  		return nil, errors.Fatalf("unable to open config file: %v\nIs there a repository at the following location?\n%v", err, s)
   574  	}
   575  
   576  	if fi.Size == 0 {
   577  		return nil, errors.New("config file has zero size, invalid repository?")
   578  	}
   579  
   580  	return be, nil
   581  }
   582  
   583  // Create the backend specified by URI.
   584  func create(s string, opts options.Options) (restic.Backend, error) {
   585  	debug.Log("parsing location %v", s)
   586  	loc, err := location.Parse(s)
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  
   591  	cfg, err := parseConfig(loc, opts)
   592  	if err != nil {
   593  		return nil, err
   594  	}
   595  
   596  	rt, err := backend.Transport(globalOptions.CACerts)
   597  	if err != nil {
   598  		return nil, err
   599  	}
   600  
   601  	switch loc.Scheme {
   602  	case "local":
   603  		return local.Create(cfg.(local.Config))
   604  	case "sftp":
   605  		return sftp.Create(cfg.(sftp.Config), SuspendSignalHandler, InstallSignalHandler)
   606  	case "s3":
   607  		return s3.Create(cfg.(s3.Config), rt)
   608  	case "gs":
   609  		return gs.Create(cfg.(gs.Config))
   610  	case "azure":
   611  		return azure.Create(cfg.(azure.Config), rt)
   612  	case "swift":
   613  		return swift.Open(cfg.(swift.Config), rt)
   614  	case "b2":
   615  		return b2.Create(globalOptions.ctx, cfg.(b2.Config), rt)
   616  	case "rest":
   617  		return rest.Create(cfg.(rest.Config), rt)
   618  	}
   619  
   620  	debug.Log("invalid repository scheme: %v", s)
   621  	return nil, errors.Fatalf("invalid scheme %q", loc.Scheme)
   622  }