diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index ab9ee41f052..019eecad8a1 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "encoding/json" "fmt" "io" "io/ioutil" @@ -81,6 +82,7 @@ type BackupOptions struct { ExcludeLargerThan string Stdin bool StdinFilename string + SummaryFilename string Tags restic.TagLists Host string FilesFrom []string @@ -115,6 +117,7 @@ func init() { f.StringVar(&backupOptions.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)") f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin") f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin") + f.StringVar(&backupOptions.SummaryFilename, "summary-filename", "", "`filename` to append summary data to") f.Var(&backupOptions.Tags, "tag", "add `tags` for the new snapshot in the format `tag[,tag,...]` (can be specified multiple times)") f.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag") @@ -710,7 +713,21 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina if !gopts.JSON && !opts.DryRun { progressPrinter.P("snapshot %s saved\n", id.Str()) } - if !success { + if opts.SummaryFilename != "" { + sf, err := os.OpenFile(opts.SummaryFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return errors.Errorf("%s: appending to summary failed: %v", opts.SummaryFilename, err) + } + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(progressReporter.FinishSummary(id)); err != nil { + return errors.Errorf("encoding summary failed: %v", err) + } + fmt.Fprintf(sf, "%s", buf.String()) + if err := sf.Close(); err != nil { + return errors.Errorf("%s: closing file failed: %v", opts.SummaryFilename, err) + } + } + if !success { return ErrInvalidSourceData } diff --git a/internal/ui/backup/json.go b/internal/ui/backup/json.go index fcc200b2a9b..db692461e75 100644 --- a/internal/ui/backup/json.go +++ b/internal/ui/backup/json.go @@ -172,7 +172,12 @@ func (b *JSONProgress) ReportTotal(item string, start time.Time, s archiver.Scan // Finish prints the finishing messages. func (b *JSONProgress) Finish(snapshotID restic.ID, start time.Time, summary *Summary, dryRun bool) { - b.print(summaryOutput{ + b.print(b.FinishSummary(snapshotID, start, summary, dryRun)) +} + +// FinishSummary returns the summary as a struct +func (b *JSONProgress) FinishSummary(snapshotID restic.ID, start time.Time, summary *Summary, dryRun bool) summaryOutput { + return summaryOutput{ MessageType: "summary", FilesNew: summary.Files.New, FilesChanged: summary.Files.Changed, @@ -188,7 +193,7 @@ func (b *JSONProgress) Finish(snapshotID restic.ID, start time.Time, summary *Su TotalDuration: time.Since(start).Seconds(), SnapshotID: snapshotID.Str(), DryRun: dryRun, - }) + } } // Reset no-op @@ -225,20 +230,3 @@ type verboseUpdate struct { TotalFiles uint `json:"total_files"` } -type summaryOutput struct { - MessageType string `json:"message_type"` // "summary" - FilesNew uint `json:"files_new"` - FilesChanged uint `json:"files_changed"` - FilesUnmodified uint `json:"files_unmodified"` - DirsNew uint `json:"dirs_new"` - DirsChanged uint `json:"dirs_changed"` - DirsUnmodified uint `json:"dirs_unmodified"` - DataBlobs int `json:"data_blobs"` - TreeBlobs int `json:"tree_blobs"` - DataAdded uint64 `json:"data_added"` - TotalFilesProcessed uint `json:"total_files_processed"` - TotalBytesProcessed uint64 `json:"total_bytes_processed"` - TotalDuration float64 `json:"total_duration"` // in seconds - SnapshotID string `json:"snapshot_id"` - DryRun bool `json:"dry_run,omitempty"` -} diff --git a/internal/ui/backup/progress.go b/internal/ui/backup/progress.go index 781ac289b18..981c3872ee1 100644 --- a/internal/ui/backup/progress.go +++ b/internal/ui/backup/progress.go @@ -19,6 +19,7 @@ type ProgressPrinter interface { CompleteItem(messageType string, item string, previous, current *restic.Node, s archiver.ItemStats, d time.Duration) ReportTotal(item string, start time.Time, s archiver.ScanStats) Finish(snapshotID restic.ID, start time.Time, summary *Summary, dryRun bool) + FinishSummary(snapshotID restic.ID, start time.Time, summary *Summary, dryRun bool) summaryOutput Reset() // ui.StdioWrapper @@ -50,6 +51,7 @@ type ProgressReporter interface { Run(ctx context.Context) error Error(item string, fi os.FileInfo, err error) error Finish(snapshotID restic.ID) + FinishSummary(snapshotID restic.ID) summaryOutput } type Summary struct { @@ -312,6 +314,11 @@ func (p *Progress) Finish(snapshotID restic.ID) { <-p.closed p.printer.Finish(snapshotID, p.start, p.summary, p.dry) } +func (p *Progress) FinishSummary(snapshotID restic.ID) summaryOutput { + // wait for the status update goroutine to shut down + <-p.closed + return p.printer.FinishSummary(snapshotID, p.start, p.summary, p.dry) +} // SetMinUpdatePause sets b.MinUpdatePause. It satisfies the // ArchiveProgressReporter interface. @@ -323,3 +330,20 @@ func (p *Progress) SetMinUpdatePause(d time.Duration) { func (p *Progress) SetDryRun() { p.dry = true } +type summaryOutput struct { + MessageType string `json:"message_type"` // "summary" + FilesNew uint `json:"files_new"` + FilesChanged uint `json:"files_changed"` + FilesUnmodified uint `json:"files_unmodified"` + DirsNew uint `json:"dirs_new"` + DirsChanged uint `json:"dirs_changed"` + DirsUnmodified uint `json:"dirs_unmodified"` + DataBlobs int `json:"data_blobs"` + TreeBlobs int `json:"tree_blobs"` + DataAdded uint64 `json:"data_added"` + TotalFilesProcessed uint `json:"total_files_processed"` + TotalBytesProcessed uint64 `json:"total_bytes_processed"` + TotalDuration float64 `json:"total_duration"` // in seconds + SnapshotID string `json:"snapshot_id"` + DryRun bool `json:"dry_run,omitempty"` +} diff --git a/internal/ui/backup/text.go b/internal/ui/backup/text.go index 998805865f1..8b242328ad0 100644 --- a/internal/ui/backup/text.go +++ b/internal/ui/backup/text.go @@ -186,3 +186,25 @@ func (b *TextProgress) Finish(snapshotID restic.ID, start time.Time, summary *Su formatDuration(time.Since(start)), ) } + +// Return finishing stats in a struct. +func (b *TextProgress) FinishSummary(snapshotID restic.ID, start time.Time, summary *Summary, dryRun bool) summaryOutput { + return summaryOutput{ + MessageType: "summary", + FilesNew: summary.Files.New, + FilesChanged: summary.Files.Changed, + FilesUnmodified: summary.Files.Unchanged, + DirsNew: summary.Dirs.New, + DirsChanged: summary.Dirs.Changed, + DirsUnmodified: summary.Dirs.Unchanged, + DataBlobs: summary.ItemStats.DataBlobs, + TreeBlobs: summary.ItemStats.TreeBlobs, + DataAdded: summary.ItemStats.DataSize + summary.ItemStats.TreeSize, + TotalFilesProcessed: summary.Files.New + summary.Files.Changed + summary.Files.Unchanged, + TotalBytesProcessed: summary.ProcessedBytes, + TotalDuration: time.Since(start).Seconds(), + SnapshotID: snapshotID.Str(), + DryRun: dryRun, + } +} +