github.com/fawick/restic@v0.1.1-0.20171126184616-c02923fbfc79/cmd/restic/cmd_backup.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/spf13/cobra" 14 15 "github.com/restic/restic/internal/archiver" 16 "github.com/restic/restic/internal/debug" 17 "github.com/restic/restic/internal/errors" 18 "github.com/restic/restic/internal/fs" 19 "github.com/restic/restic/internal/restic" 20 ) 21 22 var cmdBackup = &cobra.Command{ 23 Use: "backup [flags] FILE/DIR [FILE/DIR] ...", 24 Short: "Create a new backup of files and/or directories", 25 Long: ` 26 The "backup" command creates a new snapshot and saves the files and directories 27 given as the arguments. 28 `, 29 PreRun: func(cmd *cobra.Command, args []string) { 30 if backupOptions.Hostname == "" { 31 hostname, err := os.Hostname() 32 if err != nil { 33 debug.Log("os.Hostname() returned err: %v", err) 34 return 35 } 36 backupOptions.Hostname = hostname 37 } 38 }, 39 DisableAutoGenTag: true, 40 RunE: func(cmd *cobra.Command, args []string) error { 41 if backupOptions.Stdin && backupOptions.FilesFrom == "-" { 42 return errors.Fatal("cannot use both `--stdin` and `--files-from -`") 43 } 44 45 if backupOptions.Stdin { 46 return readBackupFromStdin(backupOptions, globalOptions, args) 47 } 48 49 return runBackup(backupOptions, globalOptions, args) 50 }, 51 } 52 53 // BackupOptions bundles all options for the backup command. 54 type BackupOptions struct { 55 Parent string 56 Force bool 57 Excludes []string 58 ExcludeFiles []string 59 ExcludeOtherFS bool 60 ExcludeIfPresent []string 61 ExcludeCaches bool 62 Stdin bool 63 StdinFilename string 64 Tags []string 65 Hostname string 66 FilesFrom string 67 TimeStamp string 68 } 69 70 var backupOptions BackupOptions 71 72 func init() { 73 cmdRoot.AddCommand(cmdBackup) 74 75 f := cmdBackup.Flags() 76 f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)") 77 f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`) 78 f.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)") 79 f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)") 80 f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems") 81 f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)") 82 f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file`) 83 f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin") 84 f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin") 85 f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)") 86 f.StringVar(&backupOptions.Hostname, "hostname", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag") 87 f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)") 88 f.StringVar(&backupOptions.TimeStamp, "time", "", "time of the backup (ex. '2012-11-01 22:08:41') (default: now)") 89 } 90 91 func newScanProgress(gopts GlobalOptions) *restic.Progress { 92 if gopts.Quiet { 93 return nil 94 } 95 96 p := restic.NewProgress() 97 p.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { 98 if IsProcessBackground() { 99 return 100 } 101 102 PrintProgress("[%s] %d directories, %d files, %s", formatDuration(d), s.Dirs, s.Files, formatBytes(s.Bytes)) 103 } 104 105 p.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { 106 PrintProgress("scanned %d directories, %d files in %s\n", s.Dirs, s.Files, formatDuration(d)) 107 } 108 109 return p 110 } 111 112 func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress { 113 if gopts.Quiet { 114 return nil 115 } 116 117 archiveProgress := restic.NewProgress() 118 119 var bps, eta uint64 120 itemsTodo := todo.Files + todo.Dirs 121 122 archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { 123 if IsProcessBackground() { 124 return 125 } 126 127 sec := uint64(d / time.Second) 128 if todo.Bytes > 0 && sec > 0 && ticker { 129 bps = s.Bytes / sec 130 if s.Bytes >= todo.Bytes { 131 eta = 0 132 } else if bps > 0 { 133 eta = (todo.Bytes - s.Bytes) / bps 134 } 135 } 136 137 itemsDone := s.Files + s.Dirs 138 139 status1 := fmt.Sprintf("[%s] %s %s/s %s / %s %d / %d items %d errors ", 140 formatDuration(d), 141 formatPercent(s.Bytes, todo.Bytes), 142 formatBytes(bps), 143 formatBytes(s.Bytes), formatBytes(todo.Bytes), 144 itemsDone, itemsTodo, 145 s.Errors) 146 status2 := fmt.Sprintf("ETA %s ", formatSeconds(eta)) 147 148 if w := stdoutTerminalWidth(); w > 0 { 149 maxlen := w - len(status2) - 1 150 151 if maxlen < 4 { 152 status1 = "" 153 } else if len(status1) > maxlen { 154 status1 = status1[:maxlen-4] 155 status1 += "... " 156 } 157 } 158 159 PrintProgress("%s%s", status1, status2) 160 } 161 162 archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { 163 fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(todo.Bytes, d)) 164 } 165 166 return archiveProgress 167 } 168 169 func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress { 170 if gopts.Quiet { 171 return nil 172 } 173 174 archiveProgress := restic.NewProgress() 175 176 var bps uint64 177 178 archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { 179 if IsProcessBackground() { 180 return 181 } 182 183 sec := uint64(d / time.Second) 184 if s.Bytes > 0 && sec > 0 && ticker { 185 bps = s.Bytes / sec 186 } 187 188 status1 := fmt.Sprintf("[%s] %s %s/s", formatDuration(d), 189 formatBytes(s.Bytes), 190 formatBytes(bps)) 191 192 if w := stdoutTerminalWidth(); w > 0 { 193 maxlen := w - len(status1) 194 195 if maxlen < 4 { 196 status1 = "" 197 } else if len(status1) > maxlen { 198 status1 = status1[:maxlen-4] 199 status1 += "... " 200 } 201 } 202 203 PrintProgress("%s", status1) 204 } 205 206 archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { 207 fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(s.Bytes, d)) 208 } 209 210 return archiveProgress 211 } 212 213 // filterExisting returns a slice of all existing items, or an error if no 214 // items exist at all. 215 func filterExisting(items []string) (result []string, err error) { 216 for _, item := range items { 217 _, err := fs.Lstat(item) 218 if err != nil && os.IsNotExist(errors.Cause(err)) { 219 Warnf("%v does not exist, skipping\n", item) 220 continue 221 } 222 223 result = append(result, item) 224 } 225 226 if len(result) == 0 { 227 return nil, errors.Fatal("all target directories/files do not exist") 228 } 229 230 return 231 } 232 233 func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error { 234 if len(args) != 0 { 235 return errors.Fatal("when reading from stdin, no additional files can be specified") 236 } 237 238 if opts.StdinFilename == "" { 239 return errors.Fatal("filename for backup from stdin must not be empty") 240 } 241 242 if gopts.password == "" { 243 return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD") 244 } 245 246 repo, err := OpenRepository(gopts) 247 if err != nil { 248 return err 249 } 250 251 lock, err := lockRepo(repo) 252 defer unlockRepo(lock) 253 if err != nil { 254 return err 255 } 256 257 err = repo.LoadIndex(context.TODO()) 258 if err != nil { 259 return err 260 } 261 262 r := &archiver.Reader{ 263 Repository: repo, 264 Tags: opts.Tags, 265 Hostname: opts.Hostname, 266 } 267 268 _, id, err := r.Archive(context.TODO(), opts.StdinFilename, os.Stdin, newArchiveStdinProgress(gopts)) 269 if err != nil { 270 return err 271 } 272 273 Verbosef("archived as %v\n", id.Str()) 274 return nil 275 } 276 277 // readFromFile will read all lines from the given filename and write them to a 278 // string array, if filename is empty readFromFile returns and empty string 279 // array. If filename is a dash (-), readFromFile will read the lines from 280 // the standard input. 281 func readLinesFromFile(filename string) ([]string, error) { 282 if filename == "" { 283 return nil, nil 284 } 285 286 var r io.Reader = os.Stdin 287 if filename != "-" { 288 f, err := os.Open(filename) 289 if err != nil { 290 return nil, err 291 } 292 defer f.Close() 293 r = f 294 } 295 296 var lines []string 297 298 scanner := bufio.NewScanner(r) 299 for scanner.Scan() { 300 line := scanner.Text() 301 // ignore empty lines 302 if line == "" { 303 continue 304 } 305 // strip comments 306 if strings.HasPrefix(line, "#") { 307 continue 308 } 309 lines = append(lines, line) 310 } 311 312 if err := scanner.Err(); err != nil { 313 return nil, err 314 } 315 316 return lines, nil 317 } 318 319 func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { 320 if opts.FilesFrom == "-" && gopts.password == "" { 321 return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD") 322 } 323 324 fromfile, err := readLinesFromFile(opts.FilesFrom) 325 if err != nil { 326 return err 327 } 328 329 // merge files from files-from into normal args so we can reuse the normal 330 // args checks and have the ability to use both files-from and args at the 331 // same time 332 args = append(args, fromfile...) 333 if len(args) == 0 { 334 return errors.Fatal("nothing to backup, please specify target files/dirs") 335 } 336 337 target := make([]string, 0, len(args)) 338 for _, d := range args { 339 if a, err := filepath.Abs(d); err == nil { 340 d = a 341 } 342 target = append(target, d) 343 } 344 345 target, err = filterExisting(target) 346 if err != nil { 347 return err 348 } 349 350 // rejectFuncs collect functions that can reject items from the backup 351 var rejectFuncs []RejectFunc 352 353 // allowed devices 354 if opts.ExcludeOtherFS { 355 f, err := rejectByDevice(target) 356 if err != nil { 357 return err 358 } 359 rejectFuncs = append(rejectFuncs, f) 360 } 361 362 // add patterns from file 363 if len(opts.ExcludeFiles) > 0 { 364 opts.Excludes = append(opts.Excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...) 365 } 366 367 if len(opts.Excludes) > 0 { 368 rejectFuncs = append(rejectFuncs, rejectByPattern(opts.Excludes)) 369 } 370 371 if opts.ExcludeCaches { 372 opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55") 373 } 374 375 rc := &rejectionCache{} 376 for _, spec := range opts.ExcludeIfPresent { 377 f, err := rejectIfPresent(spec, rc) 378 if err != nil { 379 return err 380 } 381 382 rejectFuncs = append(rejectFuncs, f) 383 } 384 385 repo, err := OpenRepository(gopts) 386 if err != nil { 387 return err 388 } 389 390 lock, err := lockRepo(repo) 391 defer unlockRepo(lock) 392 if err != nil { 393 return err 394 } 395 396 // exclude restic cache 397 if repo.Cache != nil { 398 f, err := rejectResticCache(repo) 399 if err != nil { 400 return err 401 } 402 403 rejectFuncs = append(rejectFuncs, f) 404 } 405 406 err = repo.LoadIndex(context.TODO()) 407 if err != nil { 408 return err 409 } 410 411 var parentSnapshotID *restic.ID 412 413 // Force using a parent 414 if !opts.Force && opts.Parent != "" { 415 id, err := restic.FindSnapshot(repo, opts.Parent) 416 if err != nil { 417 return errors.Fatalf("invalid id %q: %v", opts.Parent, err) 418 } 419 420 parentSnapshotID = &id 421 } 422 423 // Find last snapshot to set it as parent, if not already set 424 if !opts.Force && parentSnapshotID == nil { 425 id, err := restic.FindLatestSnapshot(context.TODO(), repo, target, []restic.TagList{}, opts.Hostname) 426 if err == nil { 427 parentSnapshotID = &id 428 } else if err != restic.ErrNoSnapshotFound { 429 return err 430 } 431 } 432 433 if parentSnapshotID != nil { 434 Verbosef("using parent snapshot %v\n", parentSnapshotID.Str()) 435 } 436 437 Verbosef("scan %v\n", target) 438 439 selectFilter := func(item string, fi os.FileInfo) bool { 440 for _, reject := range rejectFuncs { 441 if reject(item, fi) { 442 return false 443 } 444 } 445 return true 446 } 447 448 stat, err := archiver.Scan(target, selectFilter, newScanProgress(gopts)) 449 if err != nil { 450 return err 451 } 452 453 arch := archiver.New(repo) 454 arch.Excludes = opts.Excludes 455 arch.SelectFilter = selectFilter 456 457 arch.Warn = func(dir string, fi os.FileInfo, err error) { 458 // TODO: make ignoring errors configurable 459 Warnf("%s\rwarning for %s: %v\n", ClearLine(), dir, err) 460 } 461 462 timeStamp := time.Now() 463 if opts.TimeStamp != "" { 464 timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp) 465 if err != nil { 466 return errors.Fatalf("error in time option: %v\n", err) 467 } 468 } 469 470 _, id, err := arch.Snapshot(context.TODO(), newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID, timeStamp) 471 if err != nil { 472 return err 473 } 474 475 Verbosef("snapshot %s saved\n", id.Str()) 476 477 return nil 478 } 479 480 func readExcludePatternsFromFiles(excludeFiles []string) []string { 481 var excludes []string 482 for _, filename := range excludeFiles { 483 err := func() (err error) { 484 file, err := fs.Open(filename) 485 if err != nil { 486 return err 487 } 488 defer func() { 489 // return pre-close error if there was one 490 if errClose := file.Close(); err == nil { 491 err = errClose 492 } 493 }() 494 495 scanner := bufio.NewScanner(file) 496 for scanner.Scan() { 497 line := strings.TrimSpace(scanner.Text()) 498 499 // ignore empty lines 500 if line == "" { 501 continue 502 } 503 504 // strip comments 505 if strings.HasPrefix(line, "#") { 506 continue 507 } 508 509 line = os.ExpandEnv(line) 510 excludes = append(excludes, line) 511 } 512 return scanner.Err() 513 }() 514 if err != nil { 515 Warnf("error reading exclude patterns: %v:", err) 516 return nil 517 } 518 } 519 return excludes 520 }