github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/cmd/restic/cmd_backup.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 "time" 11 12 "github.com/spf13/cobra" 13 14 "github.com/restic/restic/internal/archiver" 15 "github.com/restic/restic/internal/debug" 16 "github.com/restic/restic/internal/errors" 17 "github.com/restic/restic/internal/fs" 18 "github.com/restic/restic/internal/restic" 19 ) 20 21 var cmdBackup = &cobra.Command{ 22 Use: "backup [flags] FILE/DIR [FILE/DIR] ...", 23 Short: "Create a new backup of files and/or directories", 24 Long: ` 25 The "backup" command creates a new snapshot and saves the files and directories 26 given as the arguments. 27 `, 28 PreRun: func(cmd *cobra.Command, args []string) { 29 if backupOptions.Hostname == "" { 30 hostname, err := os.Hostname() 31 if err != nil { 32 debug.Log("os.Hostname() returned err: %v", err) 33 return 34 } 35 backupOptions.Hostname = hostname 36 } 37 }, 38 DisableAutoGenTag: true, 39 RunE: func(cmd *cobra.Command, args []string) error { 40 if backupOptions.Stdin && backupOptions.FilesFrom == "-" { 41 return errors.Fatal("cannot use both `--stdin` and `--files-from -`") 42 } 43 44 if backupOptions.Stdin { 45 return readBackupFromStdin(backupOptions, globalOptions, args) 46 } 47 48 return runBackup(backupOptions, globalOptions, args) 49 }, 50 } 51 52 // BackupOptions bundles all options for the backup command. 53 type BackupOptions struct { 54 Parent string 55 Force bool 56 Excludes []string 57 ExcludeFiles []string 58 ExcludeOtherFS bool 59 ExcludeIfPresent []string 60 ExcludeCaches bool 61 Stdin bool 62 StdinFilename string 63 Tags []string 64 Hostname string 65 FilesFrom string 66 TimeStamp string 67 WithAtime bool 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 f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories") 90 } 91 92 func newScanProgress(gopts GlobalOptions) *restic.Progress { 93 if gopts.Quiet { 94 return nil 95 } 96 97 p := restic.NewProgress() 98 p.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { 99 if IsProcessBackground() { 100 return 101 } 102 103 PrintProgress("[%s] %d directories, %d files, %s", formatDuration(d), s.Dirs, s.Files, formatBytes(s.Bytes)) 104 } 105 106 p.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { 107 PrintProgress("scanned %d directories, %d files in %s\n", s.Dirs, s.Files, formatDuration(d)) 108 } 109 110 return p 111 } 112 113 func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress { 114 if gopts.Quiet { 115 return nil 116 } 117 118 archiveProgress := restic.NewProgress() 119 120 var bps, eta uint64 121 itemsTodo := todo.Files + todo.Dirs 122 123 archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { 124 if IsProcessBackground() { 125 return 126 } 127 128 sec := uint64(d / time.Second) 129 if todo.Bytes > 0 && sec > 0 && ticker { 130 bps = s.Bytes / sec 131 if s.Bytes >= todo.Bytes { 132 eta = 0 133 } else if bps > 0 { 134 eta = (todo.Bytes - s.Bytes) / bps 135 } 136 } 137 138 itemsDone := s.Files + s.Dirs 139 140 status1 := fmt.Sprintf("[%s] %s %s/s %s / %s %d / %d items %d errors ", 141 formatDuration(d), 142 formatPercent(s.Bytes, todo.Bytes), 143 formatBytes(bps), 144 formatBytes(s.Bytes), formatBytes(todo.Bytes), 145 itemsDone, itemsTodo, 146 s.Errors) 147 status2 := fmt.Sprintf("ETA %s ", formatSeconds(eta)) 148 149 if w := stdoutTerminalWidth(); w > 0 { 150 maxlen := w - len(status2) - 1 151 152 if maxlen < 4 { 153 status1 = "" 154 } else if len(status1) > maxlen { 155 status1 = status1[:maxlen-4] 156 status1 += "... " 157 } 158 } 159 160 PrintProgress("%s%s", status1, status2) 161 } 162 163 archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { 164 fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(todo.Bytes, d)) 165 } 166 167 return archiveProgress 168 } 169 170 func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress { 171 if gopts.Quiet { 172 return nil 173 } 174 175 archiveProgress := restic.NewProgress() 176 177 var bps uint64 178 179 archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { 180 if IsProcessBackground() { 181 return 182 } 183 184 sec := uint64(d / time.Second) 185 if s.Bytes > 0 && sec > 0 && ticker { 186 bps = s.Bytes / sec 187 } 188 189 status1 := fmt.Sprintf("[%s] %s %s/s", formatDuration(d), 190 formatBytes(s.Bytes), 191 formatBytes(bps)) 192 193 if w := stdoutTerminalWidth(); w > 0 { 194 maxlen := w - len(status1) 195 196 if maxlen < 4 { 197 status1 = "" 198 } else if len(status1) > maxlen { 199 status1 = status1[:maxlen-4] 200 status1 += "... " 201 } 202 } 203 204 PrintProgress("%s", status1) 205 } 206 207 archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { 208 fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(s.Bytes, d)) 209 } 210 211 return archiveProgress 212 } 213 214 // filterExisting returns a slice of all existing items, or an error if no 215 // items exist at all. 216 func filterExisting(items []string) (result []string, err error) { 217 for _, item := range items { 218 _, err := fs.Lstat(item) 219 if err != nil && os.IsNotExist(errors.Cause(err)) { 220 Warnf("%v does not exist, skipping\n", item) 221 continue 222 } 223 224 result = append(result, item) 225 } 226 227 if len(result) == 0 { 228 return nil, errors.Fatal("all target directories/files do not exist") 229 } 230 231 return 232 } 233 234 func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error { 235 if len(args) != 0 { 236 return errors.Fatal("when reading from stdin, no additional files can be specified") 237 } 238 239 if opts.StdinFilename == "" { 240 return errors.Fatal("filename for backup from stdin must not be empty") 241 } 242 243 if gopts.password == "" { 244 return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD") 245 } 246 247 repo, err := OpenRepository(gopts) 248 if err != nil { 249 return err 250 } 251 252 lock, err := lockRepo(repo) 253 defer unlockRepo(lock) 254 if err != nil { 255 return err 256 } 257 258 err = repo.LoadIndex(gopts.ctx) 259 if err != nil { 260 return err 261 } 262 263 r := &archiver.Reader{ 264 Repository: repo, 265 Tags: opts.Tags, 266 Hostname: opts.Hostname, 267 } 268 269 _, id, err := r.Archive(gopts.ctx, opts.StdinFilename, os.Stdin, newArchiveStdinProgress(gopts)) 270 if err != nil { 271 return err 272 } 273 274 Verbosef("archived as %v\n", id.Str()) 275 return nil 276 } 277 278 // readFromFile will read all lines from the given filename and write them to a 279 // string array, if filename is empty readFromFile returns and empty string 280 // array. If filename is a dash (-), readFromFile will read the lines from 281 // the standard input. 282 func readLinesFromFile(filename string) ([]string, error) { 283 if filename == "" { 284 return nil, nil 285 } 286 287 var r io.Reader = os.Stdin 288 if filename != "-" { 289 f, err := os.Open(filename) 290 if err != nil { 291 return nil, err 292 } 293 defer f.Close() 294 r = f 295 } 296 297 var lines []string 298 299 scanner := bufio.NewScanner(r) 300 for scanner.Scan() { 301 line := scanner.Text() 302 // ignore empty lines 303 if line == "" { 304 continue 305 } 306 // strip comments 307 if strings.HasPrefix(line, "#") { 308 continue 309 } 310 lines = append(lines, line) 311 } 312 313 if err := scanner.Err(); err != nil { 314 return nil, err 315 } 316 317 return lines, nil 318 } 319 320 func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { 321 if opts.FilesFrom == "-" && gopts.password == "" { 322 return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD") 323 } 324 325 fromfile, err := readLinesFromFile(opts.FilesFrom) 326 if err != nil { 327 return err 328 } 329 330 // merge files from files-from into normal args so we can reuse the normal 331 // args checks and have the ability to use both files-from and args at the 332 // same time 333 args = append(args, fromfile...) 334 if len(args) == 0 { 335 return errors.Fatal("nothing to backup, please specify target files/dirs") 336 } 337 338 target := make([]string, 0, len(args)) 339 for _, d := range args { 340 if a, err := filepath.Abs(d); err == nil { 341 d = a 342 } 343 target = append(target, d) 344 } 345 346 target, err = filterExisting(target) 347 if err != nil { 348 return err 349 } 350 351 // rejectFuncs collect functions that can reject items from the backup 352 var rejectFuncs []RejectFunc 353 354 // allowed devices 355 if opts.ExcludeOtherFS { 356 f, err := rejectByDevice(target) 357 if err != nil { 358 return err 359 } 360 rejectFuncs = append(rejectFuncs, f) 361 } 362 363 // add patterns from file 364 if len(opts.ExcludeFiles) > 0 { 365 opts.Excludes = append(opts.Excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...) 366 } 367 368 if len(opts.Excludes) > 0 { 369 rejectFuncs = append(rejectFuncs, rejectByPattern(opts.Excludes)) 370 } 371 372 if opts.ExcludeCaches { 373 opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55") 374 } 375 376 for _, spec := range opts.ExcludeIfPresent { 377 f, err := rejectIfPresent(spec) 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(gopts.ctx) 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(gopts.ctx, 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 arch.WithAccessTime = opts.WithAtime 457 458 arch.Warn = func(dir string, fi os.FileInfo, err error) { 459 // TODO: make ignoring errors configurable 460 Warnf("%s\rwarning for %s: %v\n", ClearLine(), dir, err) 461 } 462 463 timeStamp := time.Now() 464 if opts.TimeStamp != "" { 465 timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp) 466 if err != nil { 467 return errors.Fatalf("error in time option: %v\n", err) 468 } 469 } 470 471 _, id, err := arch.Snapshot(gopts.ctx, newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID, timeStamp) 472 if err != nil { 473 return err 474 } 475 476 Verbosef("snapshot %s saved\n", id.Str()) 477 478 return nil 479 } 480 481 func readExcludePatternsFromFiles(excludeFiles []string) []string { 482 var excludes []string 483 for _, filename := range excludeFiles { 484 err := func() (err error) { 485 file, err := fs.Open(filename) 486 if err != nil { 487 return err 488 } 489 defer func() { 490 // return pre-close error if there was one 491 if errClose := file.Close(); err == nil { 492 err = errClose 493 } 494 }() 495 496 scanner := bufio.NewScanner(file) 497 for scanner.Scan() { 498 line := strings.TrimSpace(scanner.Text()) 499 500 // ignore empty lines 501 if line == "" { 502 continue 503 } 504 505 // strip comments 506 if strings.HasPrefix(line, "#") { 507 continue 508 } 509 510 line = os.ExpandEnv(line) 511 excludes = append(excludes, line) 512 } 513 return scanner.Err() 514 }() 515 if err != nil { 516 Warnf("error reading exclude patterns: %v:", err) 517 return nil 518 } 519 } 520 return excludes 521 }