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 }