Repository: restic/restic
Pull request #5304 (2025-03-24 09:53:05)

backend/gs: disable GRPC API to reduce binary size bloat
<!--
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?
-----------------------------------------------------
Since cloud.google.com/go/storage v1.44.0 the GRPC API is enabled by default. However, this causes the restic binary size to explode by 20MB. So just disable it again.

<!--
Describe the changes and their purpose here, as detailed as needed.
-->

Was the change previously discussed in an issue or on the forum?
----------------------------------------------------------------
Follow up to https://github.com/restic/restic/pull/5302
<!--
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].

Please always follow these steps:
- Read the [contribution guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches).
- Enable [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).
- Run `gofmt` on the code in all commits.
- Format all commit messages in the same style as [the other commits in the repository](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
-->

- [ ] I have added tests for all code changes.
- [ ] I have added documentation for relevant changes (in the manual).
- [ ] 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'm done! This pull request is ready for review.



Patch:
diff --git a/build.go b/build.go
index 0f4e80c91..e6237bf21 100644
--- a/build.go
+++ b/build.go
@@ -53,12 +53,14 @@ import (
 
 // config contains the configuration for the program to build.
 var config = Config{
-	Name:             "restic",                                 // name of the program executable and directory
-	Namespace:        "github.com/restic/restic",               // subdir of GOPATH, e.g. "github.com/foo/bar"
-	Main:             "./cmd/restic",                           // package name for the main package
-	DefaultBuildTags: []string{"selfupdate"},                   // specify build tags which are always used
-	Tests:            []string{"./..."},                        // tests to run
-	MinVersion:       GoVersion{Major: 1, Minor: 23, Patch: 0}, // minimum Go version supported
+	Name:      "restic",                   // name of the program executable and directory
+	Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
+	Main:      "./cmd/restic",             // package name for the main package
+	// disable_grpc_modules is necessary to reduce the binary size since cloud.google.com/go/storage v1.44.0
+	// see https://github.com/googleapis/google-cloud-go/issues/11448
+	DefaultBuildTags: []string{"selfupdate", "disable_grpc_modules"}, // specify build tags which are always used
+	Tests:            []string{"./..."},                              // tests to run
+	MinVersion:       GoVersion{Major: 1, Minor: 23, Patch: 0},       // minimum Go version supported
 }
 
 // Config configures the build.
diff --git a/changelog/unreleased/issue-5259 b/changelog/unreleased/issue-5259
deleted file mode 100644
index bc9af11d4..000000000
--- a/changelog/unreleased/issue-5259
+++ /dev/null
@@ -1,16 +0,0 @@
-Bugfix: Fix rare crash in command output
-
-Some commands could in rare cases crash when trying to print status messages
-and request retries at the same time. This resulted in an error like the following:
-
-```
-panic: runtime error: slice bounds out of range [468:156]
-[...]
-github.com/restic/restic/internal/ui/termstatus.(*lineWriter).Write(...)
-	/restic/internal/ui/termstatus/stdio_wrapper.go:36 +0x136
-```
-
-This has been fixed.
-
-https://github.com/restic/restic/issues/5259
-https://github.com/restic/restic/pull/5300
diff --git a/changelog/unreleased/issue-5287 b/changelog/unreleased/issue-5287
deleted file mode 100644
index a77876038..000000000
--- a/changelog/unreleased/issue-5287
+++ /dev/null
@@ -1,8 +0,0 @@
-Enhancement: `recover` automatically runs `repair index`
-
-When trying to recover data from an interrupted snapshot, it was necessary
-to manually run `restic repair index` before runnning `restic recover`.
-This now happens automatically.
-
-https://github.com/restic/restic/issues/52897
-https://github.com/restic/restic/pull/5296
diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go
index f1c0516c2..f87303933 100644
--- a/cmd/restic/cmd_check.go
+++ b/cmd/restic/cmd_check.go
@@ -262,10 +262,10 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
 	for _, hint := range hints {
 		switch hint.(type) {
 		case *checker.ErrDuplicatePacks:
-			printer.S("%s", hint.Error())
+			term.Print(hint.Error())
 			summary.HintRepairIndex = true
 		case *checker.ErrMixedPack:
-			printer.S("%s", hint.Error())
+			term.Print(hint.Error())
 			summary.HintPrune = true
 		default:
 			printer.E("error: %v\n", hint)
@@ -274,10 +274,10 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
 	}
 
 	if summary.HintRepairIndex {
-		printer.S("Duplicate packs are non-critical, you can run `restic repair index' to correct this.\n")
+		term.Print("Duplicate packs are non-critical, you can run `restic repair index' to correct this.\n")
 	}
 	if summary.HintPrune {
-		printer.S("Mixed packs with tree and data blobs are non-critical, you can run `restic prune` to correct this.\n")
+		term.Print("Mixed packs with tree and data blobs are non-critical, you can run `restic prune` to correct this.\n")
 	}
 
 	if len(errs) > 0 {
@@ -534,7 +534,6 @@ func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
 	}
 	p.term.Error(ui.ToJSONString(status))
 }
-func (*jsonErrorPrinter) S(_ string, _ ...interface{})  {}
 func (*jsonErrorPrinter) P(_ string, _ ...interface{})  {}
 func (*jsonErrorPrinter) V(_ string, _ ...interface{})  {}
 func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
diff --git a/cmd/restic/cmd_recover.go b/cmd/restic/cmd_recover.go
index c1c71333f..10284c111 100644
--- a/cmd/restic/cmd_recover.go
+++ b/cmd/restic/cmd_recover.go
@@ -6,10 +6,7 @@ import (
 	"time"
 
 	"github.com/restic/restic/internal/errors"
-	"github.com/restic/restic/internal/repository"
 	"github.com/restic/restic/internal/restic"
-	"github.com/restic/restic/internal/ui/progress"
-	"github.com/restic/restic/internal/ui/termstatus"
 	"github.com/spf13/cobra"
 	"golang.org/x/sync/errgroup"
 )
@@ -35,41 +32,31 @@ Exit status is 12 if the password is incorrect.
 		GroupID:           cmdGroupDefault,
 		DisableAutoGenTag: true,
 		RunE: func(cmd *cobra.Command, _ []string) error {
-			term, cancel := setupTermstatus()
-			defer cancel()
-			return runRecover(cmd.Context(), globalOptions, term)
+			return runRecover(cmd.Context(), globalOptions)
 		},
 	}
 	return cmd
 }
 
-func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Terminal) error {
+func runRecover(ctx context.Context, gopts GlobalOptions) error {
 	hostname, err := os.Hostname()
 	if err != nil {
 		return err
 	}
 
-	ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
+	ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false)
 	if err != nil {
 		return err
 	}
 	defer unlock()
 
-	printer := newTerminalProgressPrinter(gopts.verbosity, term)
-
 	snapshotLister, err := restic.MemorizeList(ctx, repo, restic.SnapshotFile)
 	if err != nil {
 		return err
 	}
 
-	printer.P("ensuring index is complete\n")
-	err = repository.RepairIndex(ctx, repo, repository.RepairIndexOptions{}, printer)
-	if err != nil {
-		return err
-	}
-
-	printer.P("load index files\n")
-	bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
+	Verbosef("load index files\n")
+	bar := newIndexProgress(gopts.Quiet, gopts.JSON)
 	if err = repo.LoadIndex(ctx, bar); err != nil {
 		return err
 	}
@@ -87,15 +74,15 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
 		return err
 	}
 
-	printer.P("load %d trees\n", len(trees))
-	bar = newTerminalProgressMax(!gopts.Quiet, uint64(len(trees)), "trees loaded", term)
+	Verbosef("load %d trees\n", len(trees))
+	bar = newProgressMax(!gopts.Quiet, uint64(len(trees)), "trees loaded")
 	for id := range trees {
 		tree, err := restic.LoadTree(ctx, repo, id)
 		if ctx.Err() != nil {
 			return ctx.Err()
 		}
 		if err != nil {
-			printer.E("unable to load tree %v: %v\n", id.Str(), err)
+			Warnf("unable to load tree %v: %v\n", id.Str(), err)
 			continue
 		}
 
@@ -108,7 +95,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
 	}
 	bar.Done()
 
-	printer.P("load snapshots\n")
+	Verbosef("load snapshots\n")
 	err = restic.ForAllSnapshots(ctx, snapshotLister, repo, nil, func(_ restic.ID, sn *restic.Snapshot, _ error) error {
 		trees[*sn.Tree] = true
 		return nil
@@ -116,19 +103,19 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
 	if err != nil {
 		return err
 	}
-	printer.P("done\n")
+	Verbosef("done\n")
 
 	roots := restic.NewIDSet()
 	for id, seen := range trees {
 		if !seen {
-			printer.V("found root tree %v\n", id.Str())
+			Verboseff("found root tree %v\n", id.Str())
 			roots.Insert(id)
 		}
 	}
-	printer.S("\nfound %d unreferenced roots\n", len(roots))
+	Printf("\nfound %d unreferenced roots\n", len(roots))
 
 	if len(roots) == 0 {
-		printer.P("no snapshot to write.\n")
+		Verbosef("no snapshot to write.\n")
 		return nil
 	}
 
@@ -176,11 +163,11 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
 		return err
 	}
 
-	return createSnapshot(ctx, printer, "/recover", hostname, []string{"recovered"}, repo, &treeID)
+	return createSnapshot(ctx, "/recover", hostname, []string{"recovered"}, repo, &treeID)
 
 }
 
-func createSnapshot(ctx context.Context, printer progress.Printer, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error {
+func createSnapshot(ctx context.Context, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error {
 	sn, err := restic.NewSnapshot([]string{name}, tags, hostname, time.Now())
 	if err != nil {
 		return errors.Fatalf("unable to save snapshot: %v", err)
@@ -193,6 +180,6 @@ func createSnapshot(ctx context.Context, printer progress.Printer, name, hostnam
 		return errors.Fatalf("unable to save snapshot: %v", err)
 	}
 
-	printer.S("saved new snapshot %v\n", id.Str())
+	Printf("saved new snapshot %v\n", id.Str())
 	return nil
 }
diff --git a/cmd/restic/global.go b/cmd/restic/global.go
index 4d2ea75d5..22b09390c 100644
--- a/cmd/restic/global.go
+++ b/cmd/restic/global.go
@@ -302,7 +302,7 @@ func resolvePassword(opts *GlobalOptions, envStr string) (string, error) {
 		if err != nil {
 			return "", err
 		}
-		return strings.TrimSpace(string(output)), nil
+		return (strings.TrimSpace(string(output))), nil
 	}
 	if opts.PasswordFile != "" {
 		return loadPasswordFromFile(opts.PasswordFile)
diff --git a/go.mod b/go.mod
index 1e1ba52f2..a1b320afd 100644
--- a/go.mod
+++ b/go.mod
@@ -67,7 +67,7 @@ require (
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
-	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
 	github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/google/uuid v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index 54770e377..86134f64e 100644
--- a/go.sum
+++ b/go.sum
@@ -105,8 +105,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
-github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
diff --git a/helpers/build-release-binaries/main.go b/helpers/build-release-binaries/main.go
index 8fe8c24fb..d01bd93c1 100644
--- a/helpers/build-release-binaries/main.go
+++ b/helpers/build-release-binaries/main.go
@@ -106,7 +106,9 @@ func build(sourceDir, outputDir, goos, goarch string) (filename string) {
 	}
 	outputFile := filepath.Join(outputDir, filename)
 
-	tags := "selfupdate"
+	// disable_grpc_modules is necessary to reduce the binary size since cloud.google.com/go/storage v1.44.0
+	// see https://github.com/googleapis/google-cloud-go/issues/11448
+	tags := "selfupdate,disable_grpc_modules"
 	if opts.Tags != "" {
 		tags += "," + opts.Tags
 	}
diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go
index 4a6577d27..0b71cbacf 100644
--- a/internal/archiver/archiver.go
+++ b/internal/archiver/archiver.go
@@ -695,6 +695,10 @@ func (arch *Archiver) saveTree(ctx context.Context, snPath string, atree *tree,
 				return futureNode{}, 0, err
 			}
 
+			if err != nil {
+				return futureNode{}, 0, err
+			}
+
 			if !excluded {
 				nodes = append(nodes, fn)
 			}
diff --git a/internal/backend/http_transport.go b/internal/backend/http_transport.go
index 4e0bb6594..5a3856e41 100644
--- a/internal/backend/http_transport.go
+++ b/internal/backend/http_transport.go
@@ -80,6 +80,7 @@ func Transport(opts TransportOptions) (http.RoundTripper, error) {
 		DialContext: (&net.Dialer{
 			Timeout:   30 * time.Second,
 			KeepAlive: 30 * time.Second,
+			DualStack: true,
 		}).DialContext,
 		MaxIdleConns:          100,
 		MaxIdleConnsPerHost:   100,
diff --git a/internal/repository/index/index.go b/internal/repository/index/index.go
index 5729a689c..5a80a3c10 100644
--- a/internal/repository/index/index.go
+++ b/internal/repository/index/index.go
@@ -93,8 +93,8 @@ const (
 	indexMaxAge   = 10 * time.Minute
 )
 
-// Full returns true iff the index is "full enough" to be saved as a preliminary index.
-var Full = func(idx *Index) bool {
+// IndexFull returns true iff the index is "full enough" to be saved as a preliminary index.
+var IndexFull = func(idx *Index) bool {
 	idx.m.RLock()
 	defer idx.m.RUnlock()
 
@@ -119,7 +119,7 @@ var Full = func(idx *Index) bool {
 	return false
 }
 
-var Oversized = func(idx *Index) bool {
+var IndexOversized = func(idx *Index) bool {
 	idx.m.RLock()
 	defer idx.m.RUnlock()
 
@@ -258,8 +258,8 @@ func (idx *Index) EachByPack(ctx context.Context, packBlacklist restic.IDSet) <-
 		for packID, packByType := range byPack {
 			var result EachByPackResult
 			result.PackID = packID
-			for typ, p := range packByType {
-				for _, e := range p {
+			for typ, pack := range packByType {
+				for _, e := range pack {
 					result.Blobs = append(result.Blobs, idx.toPackedBlob(e, restic.BlobType(typ)).Blob)
 				}
 			}
@@ -511,10 +511,10 @@ func DecodeIndex(buf []byte, id restic.ID) (idx *Index, err error) {
 	}
 
 	idx = NewIndex()
-	for _, p := range idxJSON.Packs {
-		packID := idx.addToPacks(p.ID)
+	for _, pack := range idxJSON.Packs {
+		packID := idx.addToPacks(pack.ID)
 
-		for _, blob := range p.Blobs {
+		for _, blob := range pack.Blobs {
 			idx.store(packID, restic.Blob{
 				BlobHandle: restic.BlobHandle{
 					Type: blob.Type,
diff --git a/internal/repository/index/master_index.go b/internal/repository/index/master_index.go
index a41119b58..2a2f4988b 100644
--- a/internal/repository/index/master_index.go
+++ b/internal/repository/index/master_index.go
@@ -211,7 +211,7 @@ func (mi *MasterIndex) finalizeFullIndexes() []*Index {
 			continue
 		}
 
-		if Full(idx) {
+		if IndexFull(idx) {
 			debug.Log("index %p is full", idx)
 			idx.Finalize()
 			list = append(list, idx)
@@ -419,7 +419,7 @@ func (mi *MasterIndex) Rewrite(ctx context.Context, repo restic.Unpacked[restic.
 		newIndex := NewIndex()
 		for task := range rewriteCh {
 			// always rewrite indexes that include a pack that must be removed or that are not full
-			if len(task.idx.Packs().Intersect(excludePacks)) == 0 && Full(task.idx) && !Oversized(task.idx) {
+			if len(task.idx.Packs().Intersect(excludePacks)) == 0 && IndexFull(task.idx) && !IndexOversized(task.idx) {
 				// make sure that each pack is only stored exactly once in the index
 				excludePacks.Merge(task.idx.Packs())
 				// index is already up to date
@@ -435,7 +435,7 @@ func (mi *MasterIndex) Rewrite(ctx context.Context, repo restic.Unpacked[restic.
 
 			for pbs := range task.idx.EachByPack(wgCtx, excludePacks) {
 				newIndex.StorePack(pbs.PackID, pbs.Blobs)
-				if Full(newIndex) {
+				if IndexFull(newIndex) {
 					select {
 					case saveCh <- newIndex:
 					case <-wgCtx.Done():
@@ -532,7 +532,7 @@ func (mi *MasterIndex) SaveFallback(ctx context.Context, repo restic.SaverRemove
 			for pbs := range idx.EachByPack(wgCtx, excludePacks) {
 				newIndex.StorePack(pbs.PackID, pbs.Blobs)
 				p.Add(1)
-				if Full(newIndex) {
+				if IndexFull(newIndex) {
 					select {
 					case ch <- newIndex:
 					case <-wgCtx.Done():
diff --git a/internal/repository/prune.go b/internal/repository/prune.go
index 9726a6032..e22dd4c20 100644
--- a/internal/repository/prune.go
+++ b/internal/repository/prune.go
@@ -276,7 +276,7 @@ func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *i
 			ip := indexPack[blob.PackID]
 			size := uint64(blob.Length)
 			switch {
-			case ip.usedBlobs > 0, ip.duplicateBlobs == ip.unusedBlobs, count == 0:
+			case ip.usedBlobs > 0, (ip.duplicateBlobs == ip.unusedBlobs), count == 0:
 				// other used blobs in pack, only duplicate blobs or "last" occurrence ->  transition to used
 				// a pack file created by an interrupted prune run will consist of only duplicate blobs
 				// thus select such already repacked pack files
diff --git a/internal/repository/warmup.go b/internal/repository/warmup.go
index eca46692a..7d96185a7 100644
--- a/internal/repository/warmup.go
+++ b/internal/repository/warmup.go
@@ -23,7 +23,7 @@ func (job *WarmupJob) Wait(ctx context.Context) error {
 }
 
 // StartWarmup creates a new warmup job, requesting the backend to warmup the specified packs.
-func (r *Repository) StartWarmup(ctx context.Context, packs restic.IDSet) (restic.WarmupJob, error) {
+func (repo *Repository) StartWarmup(ctx context.Context, packs restic.IDSet) (restic.WarmupJob, error) {
 	handles := make([]backend.Handle, 0, len(packs))
 	for pack := range packs {
 		handles = append(
@@ -31,9 +31,9 @@ func (r *Repository) StartWarmup(ctx context.Context, packs restic.IDSet) (resti
 			backend.Handle{Type: restic.PackFile, Name: pack.String()},
 		)
 	}
-	handlesWarmingUp, err := r.be.Warmup(ctx, handles)
+	handlesWarmingUp, err := repo.be.Warmup(ctx, handles)
 	return &WarmupJob{
-		repo:             r,
+		repo:             repo,
 		handlesWarmingUp: handlesWarmingUp,
 	}, err
 }
diff --git a/internal/restic/lock_windows.go b/internal/restic/lock_windows.go
index 60ded23d0..f38840f9f 100644
--- a/internal/restic/lock_windows.go
+++ b/internal/restic/lock_windows.go
@@ -14,15 +14,12 @@ func uidGidInt(_ *user.User) (uid, gid uint32, err error) {
 
 // checkProcess will check if the process retaining the lock exists.
 // Returns true if the process exists.
-func (l *Lock) processExists() bool {
+func (l Lock) processExists() bool {
 	proc, err := os.FindProcess(l.PID)
 	if err != nil {
 		debug.Log("error searching for process %d: %v\n", l.PID, err)
 		return false
 	}
-	err = proc.Release()
-	if err != nil {
-		debug.Log("error releasing process %d: %v\n", l.PID, err)
-	}
+	proc.Release()
 	return true
 }
diff --git a/internal/ui/message.go b/internal/ui/message.go
index 6ba60f3c0..6ad5a439e 100644
--- a/internal/ui/message.go
+++ b/internal/ui/message.go
@@ -24,12 +24,6 @@ func (m *Message) E(msg string, args ...interface{}) {
 	m.term.Error(fmt.Sprintf(msg, args...))
 }
 
-// S prints a message, this is should only be used for very important messages
-// that are not errors.
-func (m *Message) S(msg string, args ...interface{}) {
-	m.term.Print(fmt.Sprintf(msg, args...))
-}
-
 // P prints a message if verbosity >= 1, this is used for normal messages which
 // are not errors.
 func (m *Message) P(msg string, args ...interface{}) {
diff --git a/internal/ui/progress/printer.go b/internal/ui/progress/printer.go
index c3e0d17a3..a2bc4c4b5 100644
--- a/internal/ui/progress/printer.go
+++ b/internal/ui/progress/printer.go
@@ -8,15 +8,9 @@ import "testing"
 type Printer interface {
 	NewCounter(description string) *Counter
 
-	// E prints to stderr
 	E(msg string, args ...interface{})
-	// S prints to stdout
-	S(msg string, args ...interface{})
-	// P prints to stdout unless quiet was passed
 	P(msg string, args ...interface{})
-	// V prints to stdout if verbose is set once
 	V(msg string, args ...interface{})
-	// VV prints to stdout if verbose is set twice
 	VV(msg string, args ...interface{})
 }
 
@@ -31,8 +25,6 @@ func (*NoopPrinter) NewCounter(_ string) *Counter {
 
 func (*NoopPrinter) E(_ string, _ ...interface{}) {}
 
-func (*NoopPrinter) S(_ string, _ ...interface{}) {}
-
 func (*NoopPrinter) P(_ string, _ ...interface{}) {}
 
 func (*NoopPrinter) V(_ string, _ ...interface{}) {}
@@ -60,10 +52,6 @@ func (p *TestPrinter) E(msg string, args ...interface{}) {
 	p.t.Logf("error: "+msg, args...)
 }
 
-func (p *TestPrinter) S(msg string, args ...interface{}) {
-	p.t.Logf("stdout: "+msg, args...)
-}
-
 func (p *TestPrinter) P(msg string, args ...interface{}) {
 	p.t.Logf("print: "+msg, args...)
 }
diff --git a/internal/ui/termstatus/stdio_wrapper.go b/internal/ui/termstatus/stdio_wrapper.go
index 4909c0fa3..233610ba3 100644
--- a/internal/ui/termstatus/stdio_wrapper.go
+++ b/internal/ui/termstatus/stdio_wrapper.go
@@ -3,7 +3,6 @@ package termstatus
 import (
 	"bytes"
 	"io"
-	"sync"
 )
 
 // WrapStdio returns line-buffering replacements for os.Stdout and os.Stderr.
@@ -13,7 +12,6 @@ func WrapStdio(term *Terminal) (stdout, stderr io.WriteCloser) {
 }
 
 type lineWriter struct {
-	m     sync.Mutex
 	buf   bytes.Buffer
 	print func(string)
 }
@@ -25,9 +23,6 @@ func newLineWriter(print func(string)) *lineWriter {
 }
 
 func (w *lineWriter) Write(data []byte) (n int, err error) {
-	w.m.Lock()
-	defer w.m.Unlock()
-
 	n, err = w.buf.Write(data)
 	if err != nil {
 		return n, err
@@ -45,9 +40,6 @@ func (w *lineWriter) Write(data []byte) (n int, err error) {
 }
 
 func (w *lineWriter) Close() error {
-	w.m.Lock()
-	defer w.m.Unlock()
-
 	if w.buf.Len() > 0 {
 		w.print(string(append(w.buf.Bytes(), '\n')))
 	}


Test patch:
diff --git a/cmd/restic/cmd_recover_integration_test.go b/cmd/restic/cmd_recover_integration_test.go
deleted file mode 100644
index c3d210200..000000000
--- a/cmd/restic/cmd_recover_integration_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package main
-
-import (
-	"context"
-	"testing"
-
-	rtest "github.com/restic/restic/internal/test"
-	"github.com/restic/restic/internal/ui/termstatus"
-)
-
-func testRunRecover(t testing.TB, gopts GlobalOptions) {
-	rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
-		return runRecover(context.TODO(), gopts, term)
-	}))
-}
-
-func TestRecover(t *testing.T) {
-	env, cleanup := withTestEnvironment(t)
-	// must list index more than once
-	env.gopts.backendTestHook = nil
-	defer cleanup()
-
-	testSetupBackupData(t, env)
-
-	// create backup and forget it afterwards
-	testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
-	ids := testListSnapshots(t, env.gopts, 1)
-	sn := testLoadSnapshot(t, env.gopts, ids[0])
-	testRunForget(t, env.gopts, ForgetOptions{}, ids[0].String())
-	testListSnapshots(t, env.gopts, 0)
-
-	testRunRecover(t, env.gopts)
-	ids = testListSnapshots(t, env.gopts, 1)
-	testRunCheck(t, env.gopts)
-	// check that the root tree is included in the snapshot
-	rtest.OK(t, runCat(context.TODO(), env.gopts, []string{"tree", ids[0].String() + ":" + sn.Tree.Str()}))
-}
diff --git a/cmd/restic/cmd_repair_index_integration_test.go b/cmd/restic/cmd_repair_index_integration_test.go
index 2b76bf4b3..9bfc93b40 100644
--- a/cmd/restic/cmd_repair_index_integration_test.go
+++ b/cmd/restic/cmd_repair_index_integration_test.go
@@ -64,11 +64,11 @@ func TestRebuildIndex(t *testing.T) {
 }
 
 func TestRebuildIndexAlwaysFull(t *testing.T) {
-	indexFull := index.Full
+	indexFull := index.IndexFull
 	defer func() {
-		index.Full = indexFull
+		index.IndexFull = indexFull
 	}()
-	index.Full = func(*index.Index) bool { return true }
+	index.IndexFull = func(*index.Index) bool { return true }
 	testRebuildIndex(t, nil)
 }
 
diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go
index dff84522a..21944a9ce 100644
--- a/cmd/restic/integration_helpers_test.go
+++ b/cmd/restic/integration_helpers_test.go
@@ -354,15 +354,6 @@ func lastSnapshot(old, new map[string]struct{}) (map[string]struct{}, string) {
 	return old, ""
 }
 
-func testLoadSnapshot(t testing.TB, gopts GlobalOptions, id restic.ID) *restic.Snapshot {
-	_, repo, unlock, err := openWithReadLock(context.TODO(), gopts, false)
-	defer unlock()
-	rtest.OK(t, err)
-	snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id)
-	rtest.OK(t, err)
-	return snapshot
-}
-
 func appendRandomData(filename string, bytes uint) error {
 	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0666)
 	if err != nil {
diff --git a/internal/repository/index/index_internal_test.go b/internal/repository/index/index_internal_test.go
index 67591ef8a..f55c9f546 100644
--- a/internal/repository/index/index_internal_test.go
+++ b/internal/repository/index/index_internal_test.go
@@ -24,7 +24,7 @@ func TestIndexOversized(t *testing.T) {
 		})
 	}
 
-	rtest.Assert(t, !Oversized(idx), "index should not be considered oversized")
+	rtest.Assert(t, !IndexOversized(idx), "index should not be considered oversized")
 
 	// Add one more blob to exceed the limit
 	idx.store(packID, restic.Blob{
@@ -36,5 +36,5 @@ func TestIndexOversized(t *testing.T) {
 		Offset: uint(indexMaxBlobs+pack.MaxHeaderEntries) * 100,
 	})
 
-	rtest.Assert(t, Oversized(idx), "index should be considered oversized")
+	rtest.Assert(t, IndexOversized(idx), "index should be considered oversized")
 }
diff --git a/internal/repository/index/master_index_test.go b/internal/repository/index/master_index_test.go
index 063af862d..ac7beb84f 100644
--- a/internal/repository/index/master_index_test.go
+++ b/internal/repository/index/master_index_test.go
@@ -466,16 +466,16 @@ func TestRewriteOversizedIndex(t *testing.T) {
 	const fullIndexCount = 1000
 
 	// replace index size checks for testing
-	originalIndexFull := index.Full
-	originalIndexOversized := index.Oversized
+	originalIndexFull := index.IndexFull
+	originalIndexOversized := index.IndexOversized
 	defer func() {
-		index.Full = originalIndexFull
-		index.Oversized = originalIndexOversized
+		index.IndexFull = originalIndexFull
+		index.IndexOversized = originalIndexOversized
 	}()
-	index.Full = func(idx *index.Index) bool {
+	index.IndexFull = func(idx *index.Index) bool {
 		return idx.Len(restic.DataBlob) > fullIndexCount
 	}
-	index.Oversized = func(idx *index.Index) bool {
+	index.IndexOversized = func(idx *index.Index) bool {
 		return idx.Len(restic.DataBlob) > 2*fullIndexCount
 	}
 
diff --git a/internal/repository/repository_test.go b/internal/repository/repository_test.go
index 0ce1eab14..1b0d47c8f 100644
--- a/internal/repository/repository_test.go
+++ b/internal/repository/repository_test.go
@@ -333,7 +333,7 @@ func TestRepositoryIncrementalIndex(t *testing.T) {
 func testRepositoryIncrementalIndex(t *testing.T, version uint) {
 	repo, _, _ := repository.TestRepositoryWithVersion(t, version)
 
-	index.Full = func(*index.Index) bool { return true }
+	index.IndexFull = func(*index.Index) bool { return true }
 
 	// add a few rounds of packs
 	for j := 0; j < 5; j++ {
diff --git a/internal/ui/termstatus/stdio_wrapper_test.go b/internal/ui/termstatus/stdio_wrapper_test.go
index 12ec1b846..1e214f1f4 100644
--- a/internal/ui/termstatus/stdio_wrapper_test.go
+++ b/internal/ui/termstatus/stdio_wrapper_test.go
@@ -1,13 +1,10 @@
 package termstatus
 
 import (
-	"context"
 	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	rtest "github.com/restic/restic/internal/test"
-	"golang.org/x/sync/errgroup"
 )
 
 func TestStdioWrapper(t *testing.T) {
@@ -85,18 +82,3 @@ func TestStdioWrapper(t *testing.T) {
 		})
 	}
 }
-
-func TestStdioWrapperConcurrentWrites(t *testing.T) {
-	// tests for race conditions when run with `go test -race ./internal/ui/termstatus`
-	w := newLineWriter(func(_ string) {})
-
-	wg, _ := errgroup.WithContext(context.TODO())
-	for range 5 {
-		wg.Go(func() error {
-			_, err := w.Write([]byte("test\n"))
-			return err
-		})
-	}
-	rtest.OK(t, wg.Wait())
-	rtest.OK(t, w.Close())
-}