Repository: restic/restic (go)

Issue:
`ls --ncdu` is broken when filtering by path
<!--

Welcome! - We kindly ask that you:

  1. Fill out the issue template below - not doing so needs a good reason.
  2. Use the forum if you have a question rather than a bug or feature request.

The forum is at: https://forum.restic.net

NOTE: Not filling out the issue template needs a good reason, as otherwise it
may take a lot longer to find the problem, not to mention it can take up a lot
more time which can otherwise be spent on development. Please also take the
time to help us debug the issue by collecting relevant information, even if
it doesn't seem to be relevant to you. Thanks!

The forum is a better place for questions about restic or general suggestions
and topics, e.g. usage or documentation questions! This issue tracker is mainly
for tracking bugs and feature requests directly relating to the development of
the software itself, rather than the project.

Thanks for understanding, and for contributing to the project!

-->


Output of `restic version`
--------------------------

Current master branch (pre 0.17.0)

What backend/service did you use to store the repository?
---------------------------------------------------------

local

Problem description / Steps to reproduce
----------------------------------------

Create a backup of the restic source code:
```bash
./restic backup ../restic
```

Run `ls --ncdu` and let it filter by some paths, then ncdu (version 2.4) fails to load the output
```bash
./restic ls --ncdu latest /restic/cmd /restic/internal | ncdu -f -
Error importing file on line 55:1: expected ',' or ']'.
```

For me currently `--ncdu` only works if no path was specified.

<!--
This section should include at least:

 * A description of the problem you are having with restic.

 * The complete command line and any environment variables you used to
   configure restic's backend access. Make sure to replace sensitive values!

 * The output of the commands, what restic prints gives may give us much
   information to diagnose the problem!

 * The more time you spend describing an easy way to reproduce the behavior (if
   this is possible), the easier it is for the project developers to fix it!
-->


Expected behavior
-----------------
Valid output readable by ncdu.
<!--
Describe what you'd like restic to do differently.
-->


Do you have any idea what may have caused this?
-----------------------------------------------
I guess I've messed up the output generated during tree traversal in #4550.
<!--
Did something noteworthy happen on your system, Internet connection, backend services, etc?
-->


Pull request description:
ls: Fix --ncdu output with path filters and fix disk size
<!--
Thank you very much for contributing code or documentation to restic! Please
fill out the following questions to make it easier for us to review your
changes.
-->

What does this PR change? What problem does it solve?
-----------------------------------------------------
Fix the ncdu error from https://github.com/restic/restic/issues/4910 . The ncdu output was missing the intermediate directories that lead up to the specified paths. In addition, skipped subfolders were not rendered correctly.

ncdu rounds a disk size of below 255bytes down to zero. To prevent this from happening round up the disk size to the next full sector.
<!--
Describe the changes and their purpose here, as detailed as needed.
-->

Was the change previously discussed in an issue or on the forum?
----------------------------------------------------------------
Fixes #4910.
<!--
Link issues and relevant forum posts here.

If this PR resolves an issue on GitHub, use "Closes #1234" so that the issue
is closed automatically when this PR is merged.
-->

Checklist
---------

<!--
You do not need to check all the boxes below all at once. Feel free to take
your time and add more commits. If you're done and ready for review, please
check the last box. Enable a checkbox by replacing [ ] with [x].
-->

- [x] I have read the [contribution guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches).
- [x] I have [enabled maintainer edits](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
- [ ] I have added tests for all code changes.
- ~~[ ] I have added documentation for relevant changes (in the manual).~~
- [x] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/restic/blob/master/changelog/TEMPLATE)).
- [x] I have run `gofmt` on the code in all commits.
- [x] All commit messages are formatted in the same style as [the other commits in the repo](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
- [x] I'm done! This pull request is ready for review.


Patch:
diff --git a/VERSION b/VERSION
index e62f3ef1c..19270385e 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.16.5-dev
+0.16.5
diff --git a/changelog/unreleased/issue-4549 b/changelog/unreleased/issue-4549
index 4829a9881..8f35b0233 100644
--- a/changelog/unreleased/issue-4549
+++ b/changelog/unreleased/issue-4549
@@ -9,3 +9,4 @@ You can use it as follows: `restic ls latest --ncdu | ncdu -f -`
 
 https://github.com/restic/restic/issues/4549
 https://github.com/restic/restic/pull/4550
+https://github.com/restic/restic/pull/4911
diff --git a/changelog/unreleased/issue-693 b/changelog/unreleased/issue-693
index 34b5b9455..054ae42ed 100644
--- a/changelog/unreleased/issue-693
+++ b/changelog/unreleased/issue-693
@@ -10,4 +10,3 @@ statistics use `restic snapshots --json` or `restic cat snapshot <snapshotID>`.
 
 https://github.com/restic/restic/issues/693
 https://github.com/restic/restic/pull/4705
-https://github.com/restic/restic/pull/4913
diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go
index c4fb32de3..6e48b010f 100644
--- a/cmd/restic/cmd_ls.go
+++ b/cmd/restic/cmd_ls.go
@@ -71,7 +71,7 @@ func init() {
 
 type lsPrinter interface {
 	Snapshot(sn *restic.Snapshot)
-	Node(path string, node *restic.Node)
+	Node(path string, node *restic.Node, isPrefixDirectory bool)
 	LeaveDir(path string)
 	Close()
 }
@@ -102,7 +102,10 @@ func (p *jsonLsPrinter) Snapshot(sn *restic.Snapshot) {
 }
 
 // Print node in our custom JSON format, followed by a newline.
-func (p *jsonLsPrinter) Node(path string, node *restic.Node) {
+func (p *jsonLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) {
+	if isPrefixDirectory {
+		return
+	}
 	err := lsNodeJSON(p.enc, path, node)
 	if err != nil {
 		Warnf("JSON encode failed: %v\n", err)
@@ -190,10 +193,13 @@ func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
 		Mtime  int64  `json:"mtime"`
 	}
 
+	const blockSize = 512
+
 	outNode := NcduNode{
-		Name:   node.Name,
-		Asize:  node.Size,
-		Dsize:  node.Size,
+		Name:  node.Name,
+		Asize: node.Size,
+		// round up to nearest full blocksize
+		Dsize:  (node.Size + blockSize - 1) / blockSize * blockSize,
 		Dev:    node.DeviceID,
 		Ino:    node.Inode,
 		NLink:  node.Links,
@@ -217,7 +223,7 @@ func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
 	return json.Marshal(outNode)
 }
 
-func (p *ncduLsPrinter) Node(path string, node *restic.Node) {
+func (p *ncduLsPrinter) Node(path string, node *restic.Node, _ bool) {
 	out, err := lsNcduNode(path, node)
 	if err != nil {
 		Warnf("JSON encode failed: %v\n", err)
@@ -249,8 +255,10 @@ type textLsPrinter struct {
 func (p *textLsPrinter) Snapshot(sn *restic.Snapshot) {
 	Verbosef("%v filtered by %v:\n", sn, p.dirs)
 }
-func (p *textLsPrinter) Node(path string, node *restic.Node) {
-	Printf("%s\n", formatNode(path, node, p.ListLong, p.HumanReadable))
+func (p *textLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) {
+	if !isPrefixDirectory {
+		Printf("%s\n", formatNode(path, node, p.ListLong, p.HumanReadable))
+	}
 }
 
 func (p *textLsPrinter) LeaveDir(_ string) {}
@@ -367,9 +375,11 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
 			return nil
 		}
 
+		printedDir := false
 		if withinDir(nodepath) {
-			// if we're within a dir, print the node
-			printer.Node(nodepath, node)
+			// if we're within a target path, print the node
+			printer.Node(nodepath, node, false)
+			printedDir = true
 
 			// if recursive listing is requested, signal the walker that it
 			// should continue walking recursively
@@ -381,12 +391,20 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
 		// if there's an upcoming match deeper in the tree (but we're not
 		// there yet), signal the walker to descend into any subdirs
 		if approachingMatchingTree(nodepath) {
+			// print node leading up to the target paths
+			if !printedDir {
+				printer.Node(nodepath, node, true)
+			}
 			return nil
 		}
 
 		// otherwise, signal the walker to not walk recursively into any
 		// subdirs
 		if node.Type == "dir" {
+			// immediately generate leaveDir if the directory is skipped
+			if printedDir {
+				printer.LeaveDir(nodepath)
+			}
 			return walker.ErrSkipNode
 		}
 		return nil
@@ -396,7 +414,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
 		ProcessNode: processNode,
 		LeaveDir: func(path string) {
 			// the root path `/` has no corresponding node and is thus also skipped by processNode
-			if withinDir(path) && path != "/" {
+			if path != "/" {
 				printer.LeaveDir(path)
 			}
 		},
diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go
index e21d0bd94..1835219ad 100644
--- a/cmd/restic/cmd_restore.go
+++ b/cmd/restic/cmd_restore.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"path/filepath"
 	"time"
 
 	"github.com/restic/restic/internal/debug"
@@ -108,10 +107,6 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
 		return errors.Fatal("--dry-run and --verify are mutually exclusive")
 	}
 
-	if opts.Delete && filepath.Clean(opts.Target) == "/" && !hasExcludes && !hasIncludes {
-		return errors.Fatal("'--target / --delete' must be combined with an include or exclude filter")
-	}
-
 	snapshotIDString := args[0]
 
 	debug.Log("restore %v to %v", snapshotIDString, opts.Target)
diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go
index ea15af019..f689e4260 100644
--- a/cmd/restic/cmd_snapshots.go
+++ b/cmd/restic/cmd_snapshots.go
@@ -190,9 +190,6 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
 		tab.AddColumn("Time", "{{ .Timestamp }}")
 		tab.AddColumn("Host", "{{ .Hostname }}")
 		tab.AddColumn("Tags  ", `{{ join .Tags "\n" }}`)
-		if hasSize {
-			tab.AddColumn("Size", `{{ .Size }}`)
-		}
 	} else {
 		tab.AddColumn("ID", "{{ .ID }}")
 		tab.AddColumn("Time", "{{ .Timestamp }}")
diff --git a/cmd/restic/global.go b/cmd/restic/global.go
index cd6a683bf..ff4fda5b6 100644
--- a/cmd/restic/global.go
+++ b/cmd/restic/global.go
@@ -43,7 +43,7 @@ import (
 	"golang.org/x/term"
 )
 
-var version = "0.16.5-dev (compiled manually)"
+const version = "0.16.5-dev (compiled manually)"
 
 // TimeFormat is the format used for all timestamps printed by restic.
 const TimeFormat = "2006-01-02 15:04:05"
diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go
index d9ff634ce..48aa209a6 100644
--- a/cmd/restic/progress.go
+++ b/cmd/restic/progress.go
@@ -53,7 +53,7 @@ func newGenericProgressMax(show bool, max uint64, description string, print func
 func newTerminalProgressMax(show bool, max uint64, description string, term *termstatus.Terminal) *progress.Counter {
 	return newGenericProgressMax(show, max, description, func(status string, final bool) {
 		if final {
-			term.SetStatus(nil)
+			term.SetStatus([]string{})
 			term.Print(status)
 		} else {
 			term.SetStatus([]string{status})
diff --git a/doc/design.rst b/doc/design.rst
index 7fb8b71b2..b80029d10 100644
--- a/doc/design.rst
+++ b/doc/design.rst
@@ -296,8 +296,8 @@ of a JSON document like the following:
     }
 
 This JSON document lists Packs and the blobs contained therein. In this
-example, the Pack ``73d04e61`` contains three data Blobs,
-the plaintext hashes are listed afterwards. The ``length`` field
+example, the Pack ``73d04e61`` contains two data Blobs and one Tree
+blob, the plaintext hashes are listed afterwards. The ``length`` field
 corresponds to ``Length(encrypted_blob)`` in the pack file header.
 Field ``uncompressed_length`` is only present for compressed blobs and
 therefore is never present in version 1 of the repository format. It is
diff --git a/go.mod b/go.mod
index 17c2337a0..3648e5c8c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,12 @@
 module github.com/restic/restic
 
 require (
-	cloud.google.com/go/storage v1.43.0
+	cloud.google.com/go/storage v1.41.0
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
 	github.com/Backblaze/blazer v0.6.1
-	github.com/anacrolix/fuse v0.3.1
+	github.com/anacrolix/fuse v0.3.1-0.20240611061849-e4651fbc304d
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/cespare/xxhash/v2 v2.3.0
 	github.com/elithrar/simple-scrypt v1.3.0
@@ -30,16 +30,16 @@ require (
 	golang.org/x/net v0.26.0
 	golang.org/x/oauth2 v0.21.0
 	golang.org/x/sync v0.7.0
-	golang.org/x/sys v0.22.0
-	golang.org/x/term v0.22.0
+	golang.org/x/sys v0.21.0
+	golang.org/x/term v0.21.0
 	golang.org/x/text v0.16.0
 	golang.org/x/time v0.5.0
-	google.golang.org/api v0.187.0
+	google.golang.org/api v0.182.0
 )
 
 require (
-	cloud.google.com/go v0.115.0 // indirect
-	cloud.google.com/go/auth v0.6.1 // indirect
+	cloud.google.com/go v0.114.0 // indirect
+	cloud.google.com/go/auth v0.4.2 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
 	cloud.google.com/go/compute/metadata v0.3.0 // indirect
 	cloud.google.com/go/iam v1.1.8 // indirect
@@ -58,7 +58,7 @@ require (
 	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
-	github.com/googleapis/gax-go/v2 v2.12.5 // indirect
+	github.com/googleapis/gax-go/v2 v2.12.4 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.6 // indirect
@@ -77,11 +77,11 @@ require (
 	go.opentelemetry.io/otel v1.24.0 // indirect
 	go.opentelemetry.io/otel/metric v1.24.0 // indirect
 	go.opentelemetry.io/otel/trace v1.24.0 // indirect
-	google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect
+	google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
 	google.golang.org/grpc v1.64.0 // indirect
-	google.golang.org/protobuf v1.34.2 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index b372fcce2..849957b0d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,17 +1,16 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
-cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
-cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38=
-cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4=
+cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY=
+cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E=
+cloud.google.com/go/auth v0.4.2 h1:sb0eyLkhRtpq5jA+a8KWw0W70YcdVca7KJ8TM0AFYDg=
+cloud.google.com/go/auth v0.4.2/go.mod h1:Kqvlz1cf1sNA0D+sYJnkPQOP+JMHkuHeIgVmCRtZOLc=
 cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
 cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
 cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
 cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
 cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
-cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
-cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
-cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
+cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0=
+cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
@@ -29,8 +28,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
 github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
 github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
-github.com/anacrolix/fuse v0.3.1 h1:oT8s3B5HFkBdLe/WKJO5MNo9iIyEtc+BhvTZYp4jhDM=
-github.com/anacrolix/fuse v0.3.1/go.mod h1:vN3X/6E+uHNjg5F8Oy9FD9I+pYxeDWeB8mNjIoxL5ds=
+github.com/anacrolix/fuse v0.3.1-0.20240611061849-e4651fbc304d h1:wCFrzeUrl7FASv1w6VRCaEAlG5u8dkSNGcj2OeJ78Vc=
+github.com/anacrolix/fuse v0.3.1-0.20240611061849-e4651fbc304d/go.mod h1:vN3X/6E+uHNjg5F8Oy9FD9I+pYxeDWeB8mNjIoxL5ds=
 github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633 h1:TO3pytMIJ98CO1nYtqbFx/iuTHi4OgIUoE2wNfDdKxw=
 github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
 github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
@@ -113,8 +112,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
-github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
-github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
+github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
+github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
@@ -278,14 +277,14 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
-golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
+golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
+golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -308,19 +307,20 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo=
-google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
+google.golang.org/api v0.182.0 h1:if5fPvudRQ78GeRx3RayIoiuV7modtErPIZC/T2bIvE=
+google.golang.org/api v0.182.0/go.mod h1:cGhjy4caqA5yXRzEhkHI8Y9mfyC2VLTlER2l08xaqtM=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d h1:PksQg4dV6Sem3/HkBX+Ltq8T0ke0PKIRBNBatoDTVls=
-google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:s7iA721uChleev562UJO2OYB0PPT9CMFjV+Ce7VJH5M=
-google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc=
-google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
+google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
+google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
+google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No=
+google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -337,8 +337,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/helpers/prepare-release/main.go b/helpers/prepare-release/main.go
index ba3de38a5..703d85e70 100644
--- a/helpers/prepare-release/main.go
+++ b/helpers/prepare-release/main.go
@@ -303,7 +303,7 @@ func generateFiles() {
 	}
 }
 
-var versionPattern = `var version = ".*"`
+var versionPattern = `const version = ".*"`
 
 const versionCodeFile = "cmd/restic/global.go"
 
@@ -313,7 +313,7 @@ func updateVersion() {
 		die("unable to write version to file: %v", err)
 	}
 
-	newVersion := fmt.Sprintf("var version = %q", opts.Version)
+	newVersion := fmt.Sprintf("const version = %q", opts.Version)
 	replace(versionCodeFile, versionPattern, newVersion)
 
 	if len(uncommittedChanges("VERSION")) > 0 || len(uncommittedChanges(versionCodeFile)) > 0 {
@@ -323,12 +323,7 @@ func updateVersion() {
 }
 
 func updateVersionDev() {
-	err := os.WriteFile("VERSION", []byte(opts.Version+"-dev\n"), 0644)
-	if err != nil {
-		die("unable to write version to file: %v", err)
-	}
-
-	newVersion := fmt.Sprintf(`var version = "%s-dev (compiled manually)"`, opts.Version)
+	newVersion := fmt.Sprintf(`const version = "%s-dev (compiled manually)"`, opts.Version)
 	replace(versionCodeFile, versionPattern, newVersion)
 
 	msg("committing cmd/restic/global.go with dev version")
diff --git a/internal/ui/backup/text.go b/internal/ui/backup/text.go
index f96746739..43e963b82 100644
--- a/internal/ui/backup/text.go
+++ b/internal/ui/backup/text.go
@@ -121,7 +121,7 @@ func (b *TextProgress) ReportTotal(start time.Time, s archiver.ScanStats) {
 // Reset status
 func (b *TextProgress) Reset() {
 	if b.term.CanUpdateStatus() {
-		b.term.SetStatus(nil)
+		b.term.SetStatus([]string{""})
 	}
 }
 
diff --git a/internal/ui/restore/text.go b/internal/ui/restore/text.go
index 235e7f085..2f0b3c01f 100644
--- a/internal/ui/restore/text.go
+++ b/internal/ui/restore/text.go
@@ -62,7 +62,7 @@ func (t *textPrinter) CompleteItem(messageType ItemAction, item string, size uin
 }
 
 func (t *textPrinter) Finish(p State, duration time.Duration) {
-	t.terminal.SetStatus(nil)
+	t.terminal.SetStatus([]string{})
 
 	timeLeft := ui.FormatDuration(duration)
 	formattedAllBytesTotal := ui.FormatBytes(p.AllBytesTotal)
diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go
index 39654cc8c..4a73ce5b3 100644
--- a/internal/ui/termstatus/status.go
+++ b/internal/ui/termstatus/status.go
@@ -315,8 +315,11 @@ func sanitizeLines(lines []string, width int) []string {
 
 // SetStatus updates the status lines.
 // The lines should not contain newlines; this method adds them.
-// Pass nil or an empty array to remove the status lines.
 func (t *Terminal) SetStatus(lines []string) {
+	if len(lines) == 0 {
+		return
+	}
+
 	// only truncate interactive status output
 	var width int
 	if t.canUpdateStatus {


Test patch:
diff --git a/cmd/restic/cmd_ls_integration_test.go b/cmd/restic/cmd_ls_integration_test.go
index 1b3c964e4..2b742d1b2 100644
--- a/cmd/restic/cmd_ls_integration_test.go
+++ b/cmd/restic/cmd_ls_integration_test.go
@@ -35,13 +35,16 @@ func TestRunLsNcdu(t *testing.T) {
 	env, cleanup := withTestEnvironment(t)
 	defer cleanup()
 
-	testRunInit(t, env.gopts)
+	testSetupBackupData(t, env)
 	opts := BackupOptions{}
 	testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
 
-	ncdu := testRunLsWithOpts(t, env.gopts, LsOptions{Ncdu: true}, []string{"latest"})
-	assertIsValidJSON(t, ncdu)
-
-	ncdu = testRunLsWithOpts(t, env.gopts, LsOptions{Ncdu: true}, []string{"latest", "/testdata"})
-	assertIsValidJSON(t, ncdu)
+	for _, paths := range [][]string{
+		{"latest"},
+		{"latest", "/testdata"},
+		{"latest", "/testdata/0", "/testdata/0/tests"},
+	} {
+		ncdu := testRunLsWithOpts(t, env.gopts, LsOptions{Ncdu: true}, paths)
+		assertIsValidJSON(t, ncdu)
+	}
 }
diff --git a/cmd/restic/cmd_ls_test.go b/cmd/restic/cmd_ls_test.go
index 828b2920e..194975053 100644
--- a/cmd/restic/cmd_ls_test.go
+++ b/cmd/restic/cmd_ls_test.go
@@ -109,7 +109,7 @@ func TestLsNodeJSON(t *testing.T) {
 
 func TestLsNcduNode(t *testing.T) {
 	for i, expect := range []string{
-		`{"name":"baz","asize":12345,"dsize":12345,"dev":0,"ino":0,"nlink":1,"notreg":false,"uid":10000000,"gid":20000000,"mode":0,"mtime":-62135596800}`,
+		`{"name":"baz","asize":12345,"dsize":12800,"dev":0,"ino":0,"nlink":1,"notreg":false,"uid":10000000,"gid":20000000,"mode":0,"mtime":-62135596800}`,
 		`{"name":"empty","asize":0,"dsize":0,"dev":0,"ino":0,"nlink":3840,"notreg":false,"uid":1001,"gid":1001,"mode":0,"mtime":-62135596800}`,
 		`{"name":"link","asize":0,"dsize":0,"dev":0,"ino":0,"nlink":0,"notreg":true,"uid":0,"gid":0,"mode":511,"mtime":-62135596800}`,
 		`{"name":"directory","asize":0,"dsize":0,"dev":0,"ino":0,"nlink":0,"notreg":false,"uid":0,"gid":0,"mode":493,"mtime":1577934245}`,
@@ -140,19 +140,19 @@ func TestLsNcdu(t *testing.T) {
 	printer.Node("/directory", &restic.Node{
 		Type: "dir",
 		Name: "directory",
-	})
+	}, false)
 	printer.Node("/directory/data", &restic.Node{
 		Type: "file",
 		Name: "data",
 		Size: 42,
-	})
+	}, false)
 	printer.LeaveDir("/directory")
 	printer.Close()
 
 	rtest.Equals(t, `[1, 2, {"time":"0001-01-01T00:00:00Z","tree":null,"paths":["/example"],"hostname":"host"},
   [
     {"name":"directory","asize":0,"dsize":0,"dev":0,"ino":0,"nlink":0,"notreg":false,"uid":0,"gid":0,"mode":0,"mtime":-62135596800},
-    {"name":"data","asize":42,"dsize":42,"dev":0,"ino":0,"nlink":0,"notreg":false,"uid":0,"gid":0,"mode":0,"mtime":-62135596800}
+    {"name":"data","asize":42,"dsize":512,"dev":0,"ino":0,"nlink":0,"notreg":false,"uid":0,"gid":0,"mode":0,"mtime":-62135596800}
   ]
 ]
 `, buf.String())
diff --git a/internal/ui/termstatus/status_test.go b/internal/ui/termstatus/status_test.go
index 2a17a905a..997a2d7b1 100644
--- a/internal/ui/termstatus/status_test.go
+++ b/internal/ui/termstatus/status_test.go
@@ -32,15 +32,6 @@ func TestSetStatus(t *testing.T) {
 	term.SetStatus([]string{"first"})
 	exp := home + clear + "first" + home
 
-	term.SetStatus([]string{""})
-	exp += home + clear + "" + home
-
-	term.SetStatus([]string{})
-	exp += home + clear + "" + home
-
-	// already empty status
-	term.SetStatus([]string{})
-
 	term.SetStatus([]string{"foo", "bar", "baz"})
 	exp += home + clear + "foo\n" + home + clear + "bar\n" +
 		home + clear + "baz" + home + up + up