diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index 50a738d5c..334063fdc 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -11,6 +11,7 @@ import ( "github.com/restic/restic/internal/cache" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/table" "github.com/spf13/cobra" ) @@ -138,7 +139,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { if err != nil { return err } - size = fmt.Sprintf("%11s", formatBytes(uint64(bytes))) + size = fmt.Sprintf("%11s", ui.FormatBytes(uint64(bytes))) } name := entry.Name() diff --git a/cmd/restic/cmd_diff.go b/cmd/restic/cmd_diff.go index 10f408ad3..0000fd18a 100644 --- a/cmd/restic/cmd_diff.go +++ b/cmd/restic/cmd_diff.go @@ -11,6 +11,7 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" ) @@ -425,8 +426,8 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args [] Printf("Others: %5d new, %5d removed\n", stats.Added.Others, stats.Removed.Others) Printf("Data Blobs: %5d new, %5d removed\n", stats.Added.DataBlobs, stats.Removed.DataBlobs) Printf("Tree Blobs: %5d new, %5d removed\n", stats.Added.TreeBlobs, stats.Removed.TreeBlobs) - Printf(" Added: %-5s\n", formatBytes(uint64(stats.Added.Bytes))) - Printf(" Removed: %-5s\n", formatBytes(uint64(stats.Removed.Bytes))) + Printf(" Added: %-5s\n", ui.FormatBytes(uint64(stats.Added.Bytes))) + Printf(" Removed: %-5s\n", ui.FormatBytes(uint64(stats.Removed.Bytes))) } return nil diff --git a/cmd/restic/cmd_prune.go b/cmd/restic/cmd_prune.go index 7918b2294..b2344d3cd 100644 --- a/cmd/restic/cmd_prune.go +++ b/cmd/restic/cmd_prune.go @@ -13,6 +13,7 @@ import ( "github.com/restic/restic/internal/pack" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" ) @@ -639,29 +640,29 @@ func decidePackAction(ctx context.Context, opts PruneOptions, gopts GlobalOption // printPruneStats prints out the statistics func printPruneStats(gopts GlobalOptions, stats pruneStats) error { - Verboseff("\nused: %10d blobs / %s\n", stats.blobs.used, formatBytes(stats.size.used)) + Verboseff("\nused: %10d blobs / %s\n", stats.blobs.used, ui.FormatBytes(stats.size.used)) if stats.blobs.duplicate > 0 { - Verboseff("duplicates: %10d blobs / %s\n", stats.blobs.duplicate, formatBytes(stats.size.duplicate)) + Verboseff("duplicates: %10d blobs / %s\n", stats.blobs.duplicate, ui.FormatBytes(stats.size.duplicate)) } - Verboseff("unused: %10d blobs / %s\n", stats.blobs.unused, formatBytes(stats.size.unused)) + Verboseff("unused: %10d blobs / %s\n", stats.blobs.unused, ui.FormatBytes(stats.size.unused)) if stats.size.unref > 0 { - Verboseff("unreferenced: %s\n", formatBytes(stats.size.unref)) + Verboseff("unreferenced: %s\n", ui.FormatBytes(stats.size.unref)) } totalBlobs := stats.blobs.used + stats.blobs.unused + stats.blobs.duplicate totalSize := stats.size.used + stats.size.duplicate + stats.size.unused + stats.size.unref unusedSize := stats.size.duplicate + stats.size.unused - Verboseff("total: %10d blobs / %s\n", totalBlobs, formatBytes(totalSize)) - Verboseff("unused size: %s of total size\n", formatPercent(unusedSize, totalSize)) + Verboseff("total: %10d blobs / %s\n", totalBlobs, ui.FormatBytes(totalSize)) + Verboseff("unused size: %s of total size\n", ui.FormatPercent(unusedSize, totalSize)) - Verbosef("\nto repack: %10d blobs / %s\n", stats.blobs.repack, formatBytes(stats.size.repack)) - Verbosef("this removes: %10d blobs / %s\n", stats.blobs.repackrm, formatBytes(stats.size.repackrm)) - Verbosef("to delete: %10d blobs / %s\n", stats.blobs.remove, formatBytes(stats.size.remove+stats.size.unref)) + Verbosef("\nto repack: %10d blobs / %s\n", stats.blobs.repack, ui.FormatBytes(stats.size.repack)) + Verbosef("this removes: %10d blobs / %s\n", stats.blobs.repackrm, ui.FormatBytes(stats.size.repackrm)) + Verbosef("to delete: %10d blobs / %s\n", stats.blobs.remove, ui.FormatBytes(stats.size.remove+stats.size.unref)) totalPruneSize := stats.size.remove + stats.size.repackrm + stats.size.unref - Verbosef("total prune: %10d blobs / %s\n", stats.blobs.remove+stats.blobs.repackrm, formatBytes(totalPruneSize)) - Verbosef("remaining: %10d blobs / %s\n", totalBlobs-(stats.blobs.remove+stats.blobs.repackrm), formatBytes(totalSize-totalPruneSize)) + Verbosef("total prune: %10d blobs / %s\n", stats.blobs.remove+stats.blobs.repackrm, ui.FormatBytes(totalPruneSize)) + Verbosef("remaining: %10d blobs / %s\n", totalBlobs-(stats.blobs.remove+stats.blobs.repackrm), ui.FormatBytes(totalSize-totalPruneSize)) unusedAfter := unusedSize - stats.size.remove - stats.size.repackrm Verbosef("unused size after prune: %s (%s of remaining size)\n", - formatBytes(unusedAfter), formatPercent(unusedAfter, totalSize-totalPruneSize)) + ui.FormatBytes(unusedAfter), ui.FormatPercent(unusedAfter, totalSize-totalPruneSize)) Verbosef("\n") Verboseff("totally used packs: %10d\n", stats.packs.used) Verboseff("partly used packs: %10d\n", stats.packs.partlyUsed) diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 547b4ae89..99d16b932 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -9,6 +9,7 @@ import ( "github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/crypto" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/walker" "github.com/minio/sha256-simd" @@ -164,9 +165,9 @@ func runStats(ctx context.Context, gopts GlobalOptions, args []string) error { Printf(" Total File Count: %d\n", stats.TotalFileCount) } if stats.TotalUncompressedSize > 0 { - Printf(" Total Uncompressed Size: %-5s\n", formatBytes(stats.TotalUncompressedSize)) + Printf(" Total Uncompressed Size: %-5s\n", ui.FormatBytes(stats.TotalUncompressedSize)) } - Printf(" Total Size: %-5s\n", formatBytes(stats.TotalSize)) + Printf(" Total Size: %-5s\n", ui.FormatBytes(stats.TotalSize)) if stats.CompressionProgress > 0 { Printf(" Compression Progress: %.2f%%\n", stats.CompressionProgress) } diff --git a/cmd/restic/format.go b/cmd/restic/format.go index 40918c897..2f14a4575 100644 --- a/cmd/restic/format.go +++ b/cmd/restic/format.go @@ -3,59 +3,10 @@ package main import ( "fmt" "os" - "time" "github.com/restic/restic/internal/restic" ) -func formatBytes(c uint64) string { - b := float64(c) - - switch { - case c > 1<<40: - return fmt.Sprintf("%.3f TiB", b/(1<<40)) - case c > 1<<30: - return fmt.Sprintf("%.3f GiB", b/(1<<30)) - case c > 1<<20: - return fmt.Sprintf("%.3f MiB", b/(1<<20)) - case c > 1<<10: - return fmt.Sprintf("%.3f KiB", b/(1<<10)) - default: - return fmt.Sprintf("%d B", c) - } -} - -func formatSeconds(sec uint64) string { - hours := sec / 3600 - sec -= hours * 3600 - min := sec / 60 - sec -= min * 60 - if hours > 0 { - return fmt.Sprintf("%d:%02d:%02d", hours, min, sec) - } - - return fmt.Sprintf("%d:%02d", min, sec) -} - -func formatPercent(numerator uint64, denominator uint64) string { - if denominator == 0 { - return "" - } - - percent := 100.0 * float64(numerator) / float64(denominator) - - if percent > 100 { - percent = 100 - } - - return fmt.Sprintf("%3.2f%%", percent) -} - -func formatDuration(d time.Duration) string { - sec := uint64(d / time.Second) - return formatSeconds(sec) -} - func formatNode(path string, n *restic.Node, long bool) string { if !long { return path diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go index 4f33e2072..3caa34a26 100644 --- a/cmd/restic/progress.go +++ b/cmd/restic/progress.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/termstatus" ) @@ -39,10 +40,11 @@ func newProgressMax(show bool, max uint64, description string) *progress.Counter return progress.New(interval, max, func(v uint64, max uint64, d time.Duration, final bool) { var status string if max == 0 { - status = fmt.Sprintf("[%s] %d %s", formatDuration(d), v, description) + status = fmt.Sprintf("[%s] %d %s", + ui.FormatDuration(d), v, description) } else { status = fmt.Sprintf("[%s] %s %d / %d %s", - formatDuration(d), formatPercent(v, max), v, max, description) + ui.FormatDuration(d), ui.FormatPercent(v, max), v, max, description) } printProgress(status, canUpdateStatus) diff --git a/internal/ui/backup/text.go b/internal/ui/backup/text.go index 03013bec1..dd3ea5b65 100644 --- a/internal/ui/backup/text.go +++ b/internal/ui/backup/text.go @@ -37,26 +37,26 @@ func (b *TextProgress) Update(total, processed Counter, errors uint, currentFile if total.Files == 0 && total.Dirs == 0 { // no total count available yet status = fmt.Sprintf("[%s] %v files, %s, %d errors", - formatDuration(time.Since(start)), - processed.Files, formatBytes(processed.Bytes), errors, + ui.FormatDuration(time.Since(start)), + processed.Files, ui.FormatBytes(processed.Bytes), errors, ) } else { var eta, percent string if secs > 0 && processed.Bytes < total.Bytes { - eta = fmt.Sprintf(" ETA %s", formatSeconds(secs)) - percent = formatPercent(processed.Bytes, total.Bytes) + eta = fmt.Sprintf(" ETA %s", ui.FormatSeconds(secs)) + percent = ui.FormatPercent(processed.Bytes, total.Bytes) percent += " " } // include totals status = fmt.Sprintf("[%s] %s%v files %s, total %v files %v, %d errors%s", - formatDuration(time.Since(start)), + ui.FormatDuration(time.Since(start)), percent, processed.Files, - formatBytes(processed.Bytes), + ui.FormatBytes(processed.Bytes), total.Files, - formatBytes(total.Bytes), + ui.FormatBytes(total.Bytes), errors, eta, ) @@ -85,69 +85,28 @@ func (b *TextProgress) Error(item string, err error) error { return nil } -func formatPercent(numerator uint64, denominator uint64) string { - if denominator == 0 { - return "" - } - - percent := 100.0 * float64(numerator) / float64(denominator) - - if percent > 100 { - percent = 100 - } - - return fmt.Sprintf("%3.2f%%", percent) -} - -func formatSeconds(sec uint64) string { - hours := sec / 3600 - sec -= hours * 3600 - min := sec / 60 - sec -= min * 60 - if hours > 0 { - return fmt.Sprintf("%d:%02d:%02d", hours, min, sec) - } - - return fmt.Sprintf("%d:%02d", min, sec) -} - -func formatDuration(d time.Duration) string { - sec := uint64(d / time.Second) - return formatSeconds(sec) -} - -func formatBytes(c uint64) string { - b := float64(c) - switch { - case c > 1<<40: - return fmt.Sprintf("%.3f TiB", b/(1<<40)) - case c > 1<<30: - return fmt.Sprintf("%.3f GiB", b/(1<<30)) - case c > 1<<20: - return fmt.Sprintf("%.3f MiB", b/(1<<20)) - case c > 1<<10: - return fmt.Sprintf("%.3f KiB", b/(1<<10)) - default: - return fmt.Sprintf("%d B", c) - } -} - // CompleteItem is the status callback function for the archiver when a // file/dir has been saved successfully. func (b *TextProgress) CompleteItem(messageType, item string, previous, current *restic.Node, s archiver.ItemStats, d time.Duration) { switch messageType { case "dir new": - b.VV("new %v, saved in %.3fs (%v added, %v stored, %v metadata)", item, d.Seconds(), formatBytes(s.DataSize), formatBytes(s.DataSizeInRepo), formatBytes(s.TreeSizeInRepo)) + b.VV("new %v, saved in %.3fs (%v added, %v stored, %v metadata)", + item, d.Seconds(), ui.FormatBytes(s.DataSize), + ui.FormatBytes(s.DataSizeInRepo), ui.FormatBytes(s.TreeSizeInRepo)) case "dir unchanged": b.VV("unchanged %v", item) case "dir modified": - b.VV("modified %v, saved in %.3fs (%v added, %v stored, %v metadata)", item, d.Seconds(), formatBytes(s.DataSize), formatBytes(s.DataSizeInRepo), formatBytes(s.TreeSizeInRepo)) + b.VV("modified %v, saved in %.3fs (%v added, %v stored, %v metadata)", + item, d.Seconds(), ui.FormatBytes(s.DataSize), + ui.FormatBytes(s.DataSizeInRepo), ui.FormatBytes(s.TreeSizeInRepo)) case "file new": - b.VV("new %v, saved in %.3fs (%v added)", item, d.Seconds(), formatBytes(s.DataSize)) + b.VV("new %v, saved in %.3fs (%v added)", item, + d.Seconds(), ui.FormatBytes(s.DataSize)) case "file unchanged": b.VV("unchanged %v", item) case "file modified": - b.VV("modified %v, saved in %.3fs (%v added, %v stored)", item, d.Seconds(), formatBytes(s.DataSize), formatBytes(s.DataSizeInRepo)) + b.VV("modified %v, saved in %.3fs (%v added, %v stored)", item, + d.Seconds(), ui.FormatBytes(s.DataSize), ui.FormatBytes(s.DataSizeInRepo)) } } @@ -155,7 +114,7 @@ func (b *TextProgress) CompleteItem(messageType, item string, previous, current func (b *TextProgress) ReportTotal(item string, start time.Time, s archiver.ScanStats) { b.V("scan finished in %.3fs: %v files, %s", time.Since(start).Seconds(), - s.Files, formatBytes(s.Bytes), + s.Files, ui.FormatBytes(s.Bytes), ) } @@ -177,11 +136,13 @@ func (b *TextProgress) Finish(snapshotID restic.ID, start time.Time, summary *Su if dryRun { verb = "Would add" } - b.P("%s to the repository: %-5s (%-5s stored)\n", verb, formatBytes(summary.ItemStats.DataSize+summary.ItemStats.TreeSize), formatBytes(summary.ItemStats.DataSizeInRepo+summary.ItemStats.TreeSizeInRepo)) + b.P("%s to the repository: %-5s (%-5s stored)\n", verb, + ui.FormatBytes(summary.ItemStats.DataSize+summary.ItemStats.TreeSize), + ui.FormatBytes(summary.ItemStats.DataSizeInRepo+summary.ItemStats.TreeSizeInRepo)) b.P("\n") b.P("processed %v files, %v in %s", summary.Files.New+summary.Files.Changed+summary.Files.Unchanged, - formatBytes(summary.ProcessedBytes), - formatDuration(time.Since(start)), + ui.FormatBytes(summary.ProcessedBytes), + ui.FormatDuration(time.Since(start)), ) } diff --git a/internal/ui/format.go b/internal/ui/format.go new file mode 100644 index 000000000..13d02f9e3 --- /dev/null +++ b/internal/ui/format.go @@ -0,0 +1,55 @@ +package ui + +import ( + "fmt" + "time" +) + +func FormatBytes(c uint64) string { + b := float64(c) + switch { + case c >= 1<<40: + return fmt.Sprintf("%.3f TiB", b/(1<<40)) + case c >= 1<<30: + return fmt.Sprintf("%.3f GiB", b/(1<<30)) + case c >= 1<<20: + return fmt.Sprintf("%.3f MiB", b/(1<<20)) + case c >= 1<<10: + return fmt.Sprintf("%.3f KiB", b/(1<<10)) + default: + return fmt.Sprintf("%d B", c) + } +} + +// FormatPercent formats numerator/denominator as a percentage. +func FormatPercent(numerator uint64, denominator uint64) string { + if denominator == 0 { + return "" + } + + percent := 100.0 * float64(numerator) / float64(denominator) + if percent > 100 { + percent = 100 + } + + return fmt.Sprintf("%3.2f%%", percent) +} + +// FormatDuration formats d as FormatSeconds would. +func FormatDuration(d time.Duration) string { + sec := uint64(d / time.Second) + return FormatSeconds(sec) +} + +// FormatSeconds formats sec as MM:SS, or HH:MM:SS if sec seconds +// is at least an hour. +func FormatSeconds(sec uint64) string { + hours := sec / 3600 + sec -= hours * 3600 + min := sec / 60 + sec -= min * 60 + if hours > 0 { + return fmt.Sprintf("%d:%02d:%02d", hours, min, sec) + } + return fmt.Sprintf("%d:%02d", min, sec) +} diff --git a/internal/ui/format_test.go b/internal/ui/format_test.go new file mode 100644 index 000000000..b6a1c13d1 --- /dev/null +++ b/internal/ui/format_test.go @@ -0,0 +1,38 @@ +package ui + +import "testing" + +func TestFormatBytes(t *testing.T) { + for _, c := range []struct { + size uint64 + want string + }{ + {0, "0 B"}, + {1023, "1023 B"}, + {1024, "1.000 KiB"}, + {5<<20 + 1<<19, "5.500 MiB"}, + {1 << 30, "1.000 GiB"}, + {2 << 30, "2.000 GiB"}, + {1<<40 - 1<<36, "960.000 GiB"}, + {1 << 40, "1.000 TiB"}, + } { + if got := FormatBytes(c.size); got != c.want { + t.Errorf("want %q, got %q", c.want, got) + } + } +} + +func TestFormatPercent(t *testing.T) { + for _, c := range []struct { + num, denom uint64 + want string + }{ + {0, 5, "0.00%"}, + {3, 7, "42.86%"}, + {99, 99, "100.00%"}, + } { + if got := FormatPercent(c.num, c.denom); got != c.want { + t.Errorf("want %q, got %q", c.want, got) + } + } +}