From 8812dcd56af987c542bff873b8568aadb52cc914 Mon Sep 17 00:00:00 2001 From: Kenny Y <24802984+kenny-y-dev@users.noreply.github.com> Date: Thu, 1 Jun 2023 21:18:18 -0400 Subject: [PATCH 1/6] Add --human-readable to ls and find output Modifies format module to add options for human readable storage size formatting, using size parsing already in ui/format. Cmd flag --human-readable added to ls and find commands. Additional option added to formatNode to support printing size in regular or new human readable format --- changelog/unreleased/issue-4159 | 13 +++++++ cmd/restic/cmd_find.go | 19 ++++++---- cmd/restic/cmd_ls.go | 6 ++- cmd/restic/format.go | 12 ++++-- cmd/restic/format_test.go | 65 +++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 changelog/unreleased/issue-4159 create mode 100644 cmd/restic/format_test.go diff --git a/changelog/unreleased/issue-4159 b/changelog/unreleased/issue-4159 new file mode 100644 index 000000000..1b3a73394 --- /dev/null +++ b/changelog/unreleased/issue-4159 @@ -0,0 +1,13 @@ +Enhancement: Add --human-readable flag to ls and find commands + +Previously, the `ls` and `find` commands showed with the `-l` option +showed size in bytes, and did not have an option for a more human +readable format that converted these longer numbers into values such +as MiB or GiB + +The new `--human-readable` option for `ls` and `find` when used with +the `-l` option will convert longer size values into more human friendly +values, with an appropriate suffix depending on the output size. For +example, a value of `14680064` will be shown as `14.000 MiB`. + +https://github.com/restic/restic/issues/4159 \ No newline at end of file diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index 6b5e32df7..e1161797b 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -51,6 +51,7 @@ type FindOptions struct { PackID, ShowPackID bool CaseInsensitive bool ListLong bool + HumanReadable bool restic.SnapshotFilter } @@ -69,6 +70,7 @@ func init() { f.BoolVar(&findOptions.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)") f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern") f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") + f.BoolVar(&findOptions.HumanReadable, "human-readable", false, "print sizes in human readable format") initMultiSnapshotFilter(f, &findOptions.SnapshotFilter, true) } @@ -104,12 +106,13 @@ func parseTime(str string) (time.Time, error) { } type statefulOutput struct { - ListLong bool - JSON bool - inuse bool - newsn *restic.Snapshot - oldsn *restic.Snapshot - hits int + ListLong bool + HumanReadable bool + JSON bool + inuse bool + newsn *restic.Snapshot + oldsn *restic.Snapshot + hits int } func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) { @@ -164,7 +167,7 @@ func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) { s.oldsn = s.newsn Verbosef("Found matching entries in snapshot %s from %s\n", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(TimeFormat)) } - Println(formatNode(path, node, s.ListLong)) + Println(formatNode(path, node, s.ListLong, s.HumanReadable)) } func (s *statefulOutput) PrintPattern(path string, node *restic.Node) { @@ -594,7 +597,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] f := &Finder{ repo: repo, pat: pat, - out: statefulOutput{ListLong: opts.ListLong, JSON: gopts.JSON}, + out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON}, ignoreTrees: restic.NewIDSet(), } diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index e8c27381c..1cd549e7c 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -50,7 +50,8 @@ Exit status is 0 if the command was successful, and non-zero if there was any er type LsOptions struct { ListLong bool restic.SnapshotFilter - Recursive bool + Recursive bool + HumanReadable bool } var lsOptions LsOptions @@ -62,6 +63,7 @@ func init() { initSingleSnapshotFilter(flags, &lsOptions.SnapshotFilter) flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories") + flags.BoolVar(&lsOptions.HumanReadable, "human-readable", false, "print sizes in human readable format") } type lsSnapshot struct { @@ -206,7 +208,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri Verbosef("snapshot %s of %v filtered by %v at %s):\n", sn.ID().Str(), sn.Paths, dirs, sn.Time) } printNode = func(path string, node *restic.Node) { - Printf("%s\n", formatNode(path, node, lsOptions.ListLong)) + Printf("%s\n", formatNode(path, node, lsOptions.ListLong, lsOptions.HumanReadable)) } } diff --git a/cmd/restic/format.go b/cmd/restic/format.go index 2f14a4575..0bc6d9fd2 100644 --- a/cmd/restic/format.go +++ b/cmd/restic/format.go @@ -5,9 +5,10 @@ import ( "os" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" ) -func formatNode(path string, n *restic.Node, long bool) string { +func formatNode(path string, n *restic.Node, long bool, human bool) string { if !long { return path } @@ -15,6 +16,11 @@ func formatNode(path string, n *restic.Node, long bool) string { var mode os.FileMode var target string + size := fmt.Sprintf("%6d", n.Size) + if human { + size = ui.FormatBytes(n.Size) + } + switch n.Type { case "file": mode = 0 @@ -33,8 +39,8 @@ func formatNode(path string, n *restic.Node, long bool) string { mode = os.ModeSocket } - return fmt.Sprintf("%s %5d %5d %6d %s %s%s", - mode|n.Mode, n.UID, n.GID, n.Size, + return fmt.Sprintf("%s %5d %5d %s %s %s%s", + mode|n.Mode, n.UID, n.GID, size, n.ModTime.Local().Format(TimeFormat), path, target) } diff --git a/cmd/restic/format_test.go b/cmd/restic/format_test.go new file mode 100644 index 000000000..9708b03d8 --- /dev/null +++ b/cmd/restic/format_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "testing" + "time" + + "github.com/restic/restic/internal/restic" + rtest "github.com/restic/restic/internal/test" +) + +func TestFormatNode(t *testing.T) { + for _, c := range []struct { + path string + restic.Node + long bool + human bool + expect string + }{ + { + path: "/test/path", + Node: restic.Node{ + Name: "baz", + Type: "file", + Size: 14680064, + UID: 1000, + GID: 2000, + ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + }, + long: false, + human: false, + expect: "/test/path", + }, + { + path: "/test/path", + Node: restic.Node{ + Name: "baz", + Type: "file", + Size: 14680064, + UID: 1000, + GID: 2000, + ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + }, + long: true, + human: false, + expect: "---------- 1000 2000 14680064 2020-01-01 22:04:05 /test/path", + }, + { + path: "/test/path", + Node: restic.Node{ + Name: "baz", + Type: "file", + Size: 14680064, + UID: 1000, + GID: 2000, + ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + }, + long: true, + human: true, + expect: "---------- 1000 2000 14.000 MiB 2020-01-01 22:04:05 /test/path", + }, + } { + r := formatNode(c.path, &c.Node, c.long, c.human) + rtest.Equals(t, r, c.expect) + } +} From 098de3554c4010c37074ffdc9e7cc266748e3aa3 Mon Sep 17 00:00:00 2001 From: Kenny Y <24802984+kenny-y-dev@users.noreply.github.com> Date: Thu, 1 Jun 2023 21:30:51 -0400 Subject: [PATCH 2/6] Add pull request link --- changelog/unreleased/issue-4159 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/unreleased/issue-4159 b/changelog/unreleased/issue-4159 index 1b3a73394..94e51d806 100644 --- a/changelog/unreleased/issue-4159 +++ b/changelog/unreleased/issue-4159 @@ -10,4 +10,5 @@ the `-l` option will convert longer size values into more human friendly values, with an appropriate suffix depending on the output size. For example, a value of `14680064` will be shown as `14.000 MiB`. -https://github.com/restic/restic/issues/4159 \ No newline at end of file +https://github.com/restic/restic/issues/4159 +https://github.com/restic/restic/pull/4351 \ No newline at end of file From 2beaa7489228c70ad55ad4af05a9c6a8fe895f97 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 8 Jun 2023 19:12:26 +0200 Subject: [PATCH 3/6] tweak changelog --- changelog/unreleased/issue-4159 | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/changelog/unreleased/issue-4159 b/changelog/unreleased/issue-4159 index 94e51d806..4f91ce240 100644 --- a/changelog/unreleased/issue-4159 +++ b/changelog/unreleased/issue-4159 @@ -1,14 +1,13 @@ -Enhancement: Add --human-readable flag to ls and find commands +Enhancement: Add `--human-readable` flag to `ls` and `find` commands -Previously, the `ls` and `find` commands showed with the `-l` option -showed size in bytes, and did not have an option for a more human -readable format that converted these longer numbers into values such -as MiB or GiB +Previously, when using the -l option with the ls and find commands, +the displayed size was always in bytes, without an option for a more +human readable format such as MiB or GiB. -The new `--human-readable` option for `ls` and `find` when used with -the `-l` option will convert longer size values into more human friendly -values, with an appropriate suffix depending on the output size. For -example, a value of `14680064` will be shown as `14.000 MiB`. +The new `--human-readable` option will convert longer size values into +more human friendly values with an appropriate suffix depending on the +output size. For example, a size of `14680064` will be shown as +`14.000 MiB`. https://github.com/restic/restic/issues/4159 -https://github.com/restic/restic/pull/4351 \ No newline at end of file +https://github.com/restic/restic/pull/4351 From 5f153109bae21ad636565c20fbd9d8e6cb02f67c Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 8 Jun 2023 19:12:49 +0200 Subject: [PATCH 4/6] Refactor formatNode --- cmd/restic/format.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/restic/format.go b/cmd/restic/format.go index 0bc6d9fd2..063cd4e71 100644 --- a/cmd/restic/format.go +++ b/cmd/restic/format.go @@ -16,9 +16,11 @@ func formatNode(path string, n *restic.Node, long bool, human bool) string { var mode os.FileMode var target string - size := fmt.Sprintf("%6d", n.Size) + var size string if human { size = ui.FormatBytes(n.Size) + } else { + size = fmt.Sprintf("%6d", n.Size) } switch n.Type { From 6ebf2dd235c9f05d5d0956c9e3c36740ee9cf9f5 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 8 Jun 2023 19:16:16 +0200 Subject: [PATCH 5/6] Reduce duplicate code in test for fomatNode --- cmd/restic/format_test.go | 49 +++++++++++++++------------------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/cmd/restic/format_test.go b/cmd/restic/format_test.go index 9708b03d8..97997945d 100644 --- a/cmd/restic/format_test.go +++ b/cmd/restic/format_test.go @@ -9,6 +9,16 @@ import ( ) func TestFormatNode(t *testing.T) { + testPath := "/test/path" + node := restic.Node{ + Name: "baz", + Type: "file", + Size: 14680064, + UID: 1000, + GID: 2000, + ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + } + for _, c := range []struct { path string restic.Node @@ -17,46 +27,25 @@ func TestFormatNode(t *testing.T) { expect string }{ { - path: "/test/path", - Node: restic.Node{ - Name: "baz", - Type: "file", - Size: 14680064, - UID: 1000, - GID: 2000, - ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), - }, + path: testPath, + Node: node, long: false, human: false, - expect: "/test/path", + expect: testPath, }, { - path: "/test/path", - Node: restic.Node{ - Name: "baz", - Type: "file", - Size: 14680064, - UID: 1000, - GID: 2000, - ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), - }, + path: testPath, + Node: node, long: true, human: false, - expect: "---------- 1000 2000 14680064 2020-01-01 22:04:05 /test/path", + expect: "---------- 1000 2000 14680064 2020-01-01 22:04:05 " + testPath, }, { - path: "/test/path", - Node: restic.Node{ - Name: "baz", - Type: "file", - Size: 14680064, - UID: 1000, - GID: 2000, - ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), - }, + path: testPath, + Node: node, long: true, human: true, - expect: "---------- 1000 2000 14.000 MiB 2020-01-01 22:04:05 /test/path", + expect: "---------- 1000 2000 14.000 MiB 2020-01-01 22:04:05 " + testPath, }, } { r := formatNode(c.path, &c.Node, c.long, c.human) From 9464c6355096153e5207cfce6716f96379252046 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 8 Jun 2023 19:18:30 +0200 Subject: [PATCH 6/6] Make formatNode test timezone independent formatNode formats the timestamp according to the current time zone. Pin the local timezone to UTC to ensure the test works everywhere. --- cmd/restic/format_test.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/restic/format_test.go b/cmd/restic/format_test.go index 97997945d..689bd27a5 100644 --- a/cmd/restic/format_test.go +++ b/cmd/restic/format_test.go @@ -9,6 +9,13 @@ import ( ) func TestFormatNode(t *testing.T) { + // overwrite time zone to ensure the data is formatted reproducibly + tz := time.Local + time.Local = time.UTC + defer func() { + time.Local = tz + }() + testPath := "/test/path" node := restic.Node{ Name: "baz", @@ -38,17 +45,17 @@ func TestFormatNode(t *testing.T) { Node: node, long: true, human: false, - expect: "---------- 1000 2000 14680064 2020-01-01 22:04:05 " + testPath, + expect: "---------- 1000 2000 14680064 2020-01-02 03:04:05 " + testPath, }, { path: testPath, Node: node, long: true, human: true, - expect: "---------- 1000 2000 14.000 MiB 2020-01-01 22:04:05 " + testPath, + expect: "---------- 1000 2000 14.000 MiB 2020-01-02 03:04:05 " + testPath, }, } { r := formatNode(c.path, &c.Node, c.long, c.human) - rtest.Equals(t, r, c.expect) + rtest.Equals(t, c.expect, r) } }