github.com/fawick/restic@v0.1.1-0.20171126184616-c02923fbfc79/cmd/restic/global.go (about)

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