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 }