Compare commits

...
Author SHA1 Message Date
dependabot[bot] 3baba6519e build(deps): bump re-actors/alls-green from 1.2.2 to 1.3.0
Bumps [re-actors/alls-green](https://github.com/re-actors/alls-green) from 1.2.2 to 1.3.0.
- [Release notes](https://github.com/re-actors/alls-green/releases)
- [Commits](https://github.com/re-actors/alls-green/compare/05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe...b5b5b37504aa4183270bd3d855c52a67f212be35)

---
updated-dependencies:
- dependency-name: re-actors/alls-green
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-01 01:43:57 +00:00
Etienne D ba802d42b7 fix(swift): store APIKey as SecretString to prevent debug log leak (#22007) 2026-08-29 20:11:48 +00:00
CAOShurong 46ea365629 docs: fix two dead links (PGP key URL, MinIO docs URL) (#22028) 2026-08-29 19:07:37 +00:00
CAOShurong fb5e9df357 docs: fix contradiction about which files are encrypted (#22029) 2026-08-29 19:07:11 +00:00
Michael Eischer b593ba305b Apply go fix ./... (#22037) 2026-08-29 21:02:05 +02:00
Buggfix42 28d802df8c [Documentation] A few minor improvements (#21998) 2026-08-29 17:47:29 +00:00
GuitarBilly 0946f45de6 Update 110_talks.rst (#21989) 2026-08-29 19:44:43 +02:00
dependabot[bot] 5895e6d059 build(deps): bump cloud.google.com/go/storage from 1.63.0 to 1.64.0 (#21996)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-29 19:43:41 +02:00
Michael Eischer e982b6e7b5 CI: add size labels to PRs (#22036) 2026-08-29 19:39:04 +02:00
Jörn Bethune 479cf1fb3d documentation: Fix incorrect snapshot ID (#22017) 2026-08-29 17:33:20 +00:00
Ricardo Mendonça Ferreira dbaa8cf124 Update documentation for environment variable syntax (#22026) 2026-08-29 17:30:52 +00:00
dependabot[bot] a80be1478a build(deps): bump google.golang.org/api from 0.285.0 to 0.291.0 (#21995)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 22:24:27 +02:00
dependabot[bot] 7bfa32a90a build(deps): bump github.com/pkg/sftp from 1.13.10 to 1.13.11 (#21994)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 22:23:52 +02:00
dependabot[bot] 63b583c4ea build(deps): bump github.com/minio/minio-go/v7 from 7.1.0 to 7.2.1 (#21993)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 22:22:40 +02:00
dependabot[bot] 85bf4430b8 build(deps): bump actions/setup-go from 6 to 7 (#21992)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 22:21:34 +02:00
dependabot[bot] e2b43a5f1f build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob from 1.7.0 to 1.8.0 (#21991)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 22:21:07 +02:00
dependabot[bot] 607aba3809 build(deps): bump docker/login-action from 4.2.0 to 4.6.0 (#21990)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 22:20:40 +02:00
Michael Eischer c6074f82fb Merge pull request #21978 from MichaelEischer/go-fix2
Run `go fix ./...`
2026-08-01 22:17:53 +02:00
Arpit Jain 8baffc4027 repository: reject key and config files that are too short (#21977)
Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
2026-07-22 20:38:07 +00:00
Michael Eischer f215365af9 Apply go fix -waitgroup ./... 2026-07-22 22:27:21 +02:00
Michael Eischer 14e523a1d9 Apply go fix -testingcontext ./... 2026-07-22 22:27:15 +02:00
Michael Eischer 58f92708c2 Apply go fix -stringsseq ./... 2026-07-22 22:27:07 +02:00
Michael Eischer 31e96f4ec9 Apply go fix -stringsbuilder ./... 2026-07-22 22:26:50 +02:00
Michael Eischer a2cb015591 Apply go fix -slicescontains ./... 2026-07-22 22:26:33 +02:00
Michael Eischer e2495b72bb Apply go fix -reflecttypefor ./... 2026-07-22 22:26:27 +02:00
Michael Eischer 2c169edad4 Apply go fix -rangeint ./... 2026-07-22 22:26:19 +02:00
Michael Eischer fb36a7f59b Apply go fix -omitzero ./... 2026-07-22 22:26:07 +02:00
Michael Eischer 8ed263c6dd Apply go fix -minmax ./... 2026-07-22 22:25:56 +02:00
Michael Eischer 0e4f288e83 Apply go fix -mapsloop ./... 2026-07-22 22:25:47 +02:00
Michael Eischer da4d8f5851 Apply go fix -fmtappendf ./... 2026-07-22 22:25:26 +02:00
Michael Eischer 32e9869cc8 Apply go fix -any ./... 2026-07-22 22:25:18 +02:00
Junker der Provinz d4088aa09b doc: describe the cadence of JSON status messages (#21954) 2026-07-15 20:38:38 +00:00
schrumpelnase 35dcda418c docs(README.md): fixed label of the link to the mastodon profile (#21958) 2026-07-15 20:32:26 +00:00
Michael Eischer af4f50223e ls: only keep fields required for sorting in memory (#21949) 2026-07-15 22:22:55 +02:00
Michael Eischer 9e007fa3cc Merge pull request #21937 from brainexe/reduce-index-memory-packindex-uint32
Reduce in-memory index entry size by storing packIndex as uint32
2026-07-15 22:16:40 +02:00
dependabot[bot] a4d85ce299 build(deps): bump the golang-x-deps group across 1 directory with 6 updates (#21923)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-15 21:32:37 +02:00
dependabot[bot] 9b0ee37697 build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity from 1.13.1 to 1.14.0 (#21925)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-15 21:31:44 +02:00
Matthias Dötsch 905ca56ee8 Address review feedback on #21937:
- addToPacks now returns uint32 directly and the value is passed through
  store() to indexMap.add unchanged, instead of casting between int and
  uint32.
- merge() appends idx2.packs to idx.packs without going through
  addToPacks, so it now checks the pack count against the uint32 limit
  itself + error if exceeded.
- optimize changelog message
2026-07-13 07:25:08 +02:00
dependabot[bot] 987caba408 build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azcore from 1.21.1 to 1.22.0 (#21926)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 22:46:11 +02:00
dependabot[bot] bfca18c1b0 build(deps): bump actions/checkout from 6 to 7 (#21922)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 22:20:22 +02:00
dependabot[bot] dba493cb6c build(deps): bump github.com/klauspost/compress from 1.18.6 to 1.18.7 (#21924)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 22:17:43 +02:00
dependabot[bot] 512d0577d6 build(deps): bump cloud.google.com/go/storage from 1.62.2 to 1.63.0 (#21927)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 22:16:48 +02:00
Matthias Dötsch e428de5f84 Reduce in-memory index entry size by storing packIndex as uint32
Each blob in a repository requires one index entry in memory, which is one
of the largest contributors to restic's memory usage. The indexEntry.packIndex
field, an index into the per-Index packs slice, was stored as a machine-word
int (8 bytes on amd64) although the value is always a small array index.

Store it as a uint32 instead. This shrinks indexEntry from 64 to 56 bytes on
64-bit platforms (56 % 8 == 0, so no padding is added), reducing index memory
by roughly 12% -- about 100 MB for a repository with ~13 million blobs, as
confirmed by profiling. uint32 allows 4.29 billion packs (>30 billion blobs at
the assumed 8 blobs/pack), far beyond any real repository; addToPacks guards
against overflow defensively. The on-disk repository format is unaffected.
2026-07-08 06:01:15 +00:00
Michael Eischer d8ef26afa4 Merge branch 'patch-release' 2026-07-05 16:27:43 +02:00
astro-stan cc93a94e15 feat: Add support for setting --ignore-ctime and --ignore-inode via environment variables (#5730) 2026-06-28 15:36:58 +02:00
Winfried PlappertandMichael Eischer 62991338ea replace FindFilteredSnapshots() with (*data.SnapshotFilter).FindAll() (#21912)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-06-28 13:31:06 +00:00
Winfried PlappertandMichael Eischer 75de8b54e6 restic check and restic repair packs: treat missing packfiles the same as damaged and truncated packfiles (#21845)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-06-28 12:09:42 +02:00
Michael Eischer e4056e70a8 Merge pull request #21888 from MichaelEischer/chunker-factory
Move chunker construction to repository and switch to BaseChunker
2026-06-27 22:56:07 +02:00
Michael Eischer 8cc69782b5 repository: expose ZeroChunk via chunker factory 2026-06-27 22:23:59 +02:00
Michael Eischer ccb828e87b repository: add ChunkerFactory and decouple archiver from chunker
Introduce restic.Chunker and ChunkerFactory interfaces with a
repository-backed implementation. The archiver obtains chunkers via
ChunkerFactory() from the repository.
2026-06-27 22:13:41 +02:00
Michael Eischer 70a45791b2 archiver: switch file saver from Chunker to BaseChunker
Update the chunker dependency. Move the chunk memory handling into the
fileSaver. This minimizes the logic required in a chunker
implementation.

readNextChunk is a direct derivative of Chunker.Next.
2026-06-27 22:13:41 +02:00
Winfried PlappertandMichael Eischer 9e1a526611 Fix broken snapshots via restic repair snapshots --forget (#21907)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-06-27 22:09:10 +02:00
Michael Eischer aa94be2dbd Merge pull request #21883 from MichaelEischer/fix-find-pack
find: improve blob resolution for broken pack files
2026-06-26 22:33:38 +02:00
Michael Eischer 804ff4bc95 find: add test for listing blobs from corrupt pack files 2026-06-26 22:16:55 +02:00
Michael Eischer 9670cd7459 find: fix fallback to index if pack cannot be listed
`find` would only look up completely missing pack files in the index. A
still existing but broken pack file would result in an error.
2026-06-26 22:16:55 +02:00
Michael Eischer 634e9b2411 find: check blobIDs/treeIDs presence by length
Depending on the code path, the map could be initialized but stay empty.
2026-06-26 21:46:30 +02:00
Gilbert Gilb's db03aed816 feat(backends/s3): add warmup support for check command (#5248) 2026-06-25 20:46:55 +02:00
Michael Eischer 4e8eea7ab4 Merge pull request #21902 from restic/speedup-test
Cut local test suite execution time in half
2026-06-24 21:34:55 +02:00
Michael Eischer 981adc47ac snapshots: improve wording for timezone information (#21913) 2026-06-24 19:49:07 +02:00
Michael Eischer 7134577c56 use constants from cobra for shell completion helper names (#21905) 2026-06-22 22:58:26 +02:00
Ricardo Sawir 55e335ec6c fuse: reset treeCache on snapshot reload to fix stale latest symlink (#21873)
The treeCache in SnapshotsDir was never cleared when snapshots were
reloaded. This caused the "latest" symlink to keep pointing to the
previous snapshot even after new snapshots were added.

Add a generation counter to SnapshotsDirStructure that is incremented
whenever the directory structure is rebuilt (in makeDirs). The
treeCache checks this generation on each lookup and resets itself
when the generation changes, ensuring cached nodes (including symlinks)
are refreshed after a snapshot reload.
2026-06-22 18:31:51 +00:00
Winfried Plappert d30d6b6281 backup - show excluded files and directories in verbose mode - text/JSON (#21887) 2026-06-21 16:11:42 +00:00
Eyüp Can Akman 8d7679c831 backup: skip source paths that cannot be accessed (#21852) 2026-06-21 17:16:16 +02:00
Michael Eischer d9dd1b7534 archiver: ignore duplicate but excluded directory entry (#21900) 2026-06-21 15:49:08 +02:00
Michael Eischer 1151d70655 Merge pull request #21875 from MichaelEischer/fix-snapshots-latest-group-by
Fix default grouping of `snapshots --latest <n>`
2026-06-21 15:46:49 +02:00
Michael Eischer 369da8a770 snapshots: revert default --lastest behavior to pre-0.19.0
the changed behavior now only applies when using `--group-by`.
2026-06-21 15:28:22 +02:00
Michael Eischer 9f84c80056 snapshots: cleanup handling of deprecated --last option 2026-06-21 15:28:22 +02:00
Michael Eischer 6070462465 backup: move options finalization to BackupOptions.Finalize 2026-06-21 15:28:22 +02:00
Michael Eischer 1029fe6dd0 list packs snapshot: include actual error in error message (#21903) 2026-06-21 15:27:19 +02:00
Michael Eischer 8f00b335b4 rclone/fs: parallelize tests 2026-06-20 22:43:03 +02:00
Michael Eischer c6a064b68b mount: disable debug log for test
I don't recall any recent problems with that test.
2026-06-20 22:04:07 +02:00
Michael Eischer 8d191c19a5 archiver: decrease sleeps for TestFileChanged test
Modern filesystems provide microsecond granularity or better. The old
special case for macOS is removed as Go 1.25 (minimum for restic)
requires at least macOS 12, which uses APFS instead of the old HFS+.
2026-06-20 21:55:46 +02:00
Michael Eischer d5267f03e0 use fastest compression in integration tests 2026-06-20 20:00:17 +02:00
Michael Eischer 5683224a3c layout: skip creation of pack subdirectories in integration tests
Each time a local backend is created, the local backend also creates all
shard subdirectories. For the integration tests this has the downside
that this results in ~120 (number of test) * 256 (number of directories)
= 30k directories that are created unnecessarily. This significantly
slows down test execution and cleanup.
2026-06-20 20:00:17 +02:00
Michael Eischer 71064fac10 mount: decrease sleep to speed up test 2026-06-20 20:00:17 +02:00
Michael Eischer 52cde35aea try deleting a directory before resetting the readonly status in tests 2026-06-20 20:00:17 +02:00
Michael Eischer f394723aa2 inject crypto keys for integration tests 2026-06-20 20:00:17 +02:00
Michael Eischer 072305c5b3 repository: run version sub-tests of TestAllVersions in parallel
TestRepositoryIncrementalIndex no longer modified index.Full as this
modified global state and also had no effect on the test result.
2026-06-20 19:59:37 +02:00
Michael Eischer a1420ea6c7 repository: use separate rand instance per test
Use separate instances to prevent data races when executing tests in
parallel.
2026-06-20 19:50:10 +02:00
Michael Eischer a3be3bd9e1 backend/test: gradually increase timeout for TestListCancel to speed up fast backends 2026-06-20 19:46:22 +02:00
Michael Eischer b24d05bcb4 skip key decryption in tests 2026-06-20 19:46:22 +02:00
Michael Eischer c021b8cde0 decrease test data sizes 2026-06-20 19:46:22 +02:00
Michael Eischer 18e5c446e0 data/fs: faster comparison in tests 2026-06-20 19:46:22 +02:00
Michael Eischer f91afb2fa8 repository: run TestPruneSmall in parallel to other tests 2026-06-20 19:46:22 +02:00
Michael Eischer 2148508050 repository/checker: speed up test repo creation
data.TestCreateSnapshot is much faster than archiver.TestSnapshot
2026-06-20 19:46:22 +02:00
Michael Eischer 37e8f50aeb repository: decrease test data size for streamPack 2026-06-20 19:46:22 +02:00
Michael Eischer 7e5eaf1ead crypto: decrease parameters for calibrate test 2026-06-20 19:46:22 +02:00
Michael Eischer aae0985673 index: decrease depth of test repos 2026-06-20 19:46:22 +02:00
Michael Eischer f9d953d4cd index: test ForAllIndex with generated repo 2026-06-20 19:46:22 +02:00
Michael Eischer 69a3ba5b33 index: speed up oversized test data generation 2026-06-20 19:46:22 +02:00
Michael Eischer c183fe2035 restore: speed up test data generation in newTestRepo 2026-06-20 19:46:22 +02:00
Michael Eischer 1174e97b30 restic: speed up repository config initialization in tests
Avoid redundant polynomial generation, which takes a millisecond or so.
2026-06-20 19:46:22 +02:00
Michael Eischer 440d925467 repository: default to fastest compression in tests 2026-06-20 19:46:22 +02:00
Michael Eischer 28521d27c1 Merge pull request #21897 from MichaelEischer/sftp-fix-chmod
backend/sftp: fix removing files on windows sftp
2026-06-20 19:29:49 +02:00
Michael Eischer af64caf5d9 Merge pull request #21885 from MichaelEischer/separate-backend-restic-filetype
Separate backend.FileType from restic.FileType
2026-06-20 19:24:51 +02:00
Michael Eischer 9ab5fc59c2 restic: split FileType for backend.FileType
Equality of constants is enforced via internal/repository/filetype.go
using compile time checks.
2026-06-20 19:15:40 +02:00
Michael Eischer 6c509f7ac1 Correctly pair backend/restic.FileType with APIs
Use backend.FileType to interact with the backend. And restic.FileType
to interact with the repository.
2026-06-20 19:15:40 +02:00
Michael Eischer ea699bb0ba backend/rest: fix flaky test cleanup (#21901) 2026-06-20 19:14:54 +02:00
Michael Eischer 532f36a5a8 archiver: pass ItemAction to progress reporter instead of data.Node (#21886) 2026-06-20 19:06:45 +02:00
Michael Eischer 57ac5d107c Merge pull request #21882 from MichaelEischer/untangle-imports
Untangle imports of repository, restorer, fs
2026-06-20 18:24:50 +02:00
Michael Eischer 2f31cde517 internal/restorer: invert dependency direction to ui/restorer
The restorer imported ui/restorer which leaks ui logic into the restorer
core. Swap the direction by letting the restorer use a ProgressReporter
interface + associated constants.
2026-06-20 17:49:20 +02:00
Michael Eischer 5cd39d01c1 internal/data: extract HardlinkIndex from restorer
The HardlinkIndex is also used by `cmd_stats`, thus place it in a shared
package.
2026-06-20 17:49:20 +02:00
Michael Eischer 61708772a2 mount: use existing HasPathPrefix helper
replaces the custom isInside helper to check path nesting
2026-06-20 17:49:20 +02:00
Michael Eischer d9d54a505e restic: move Printer interface from internal/ui/progress
Move Printer and NewNoopPrinter to internal/restic so repository does
not have to import the ui packages.
2026-06-20 17:49:20 +02:00
Michael Eischer 0f4236cb39 repository: return unlock func from LockRepo
Drop the Unlocker interface and return the unlock callback directly
from LockRepo, simplifying callers that only need to defer unlock().
2026-06-20 17:48:35 +02:00
Michael Eischer 4d1b9cef63 internal/fileio: extract low-level file I/O from internal/fs
Move TempFile and PreallocateFile into internal/fileio. internal/fs
primarily focuses on converting between data.Node and the actual
filesystem state. Extract the two methods to not pull in unnecessary
dependencies.
2026-06-20 17:48:35 +02:00
Winfried PlappertandMichael Eischer 284daaf0b4 restic list pack snapshotID - create lists of packfiles for a given snapshot as text (#5396)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-06-20 15:46:44 +00:00
Michael Eischer 980f194360 fs: print missing error if xattr cannot be read (#21896) 2026-06-20 17:11:16 +02:00
Michael Eischer d795988fa8 Misc error handling fixes (#21884)
* forget: fix error handling if snapshot key cannot be marshaled

* backend/b2: correctly close file if too short

* backend/rest: correctly close body on out of bounds read

* backend/sftp: fix file handle leak if chmod for a file fails

* backend/util: fix errorhandling in DefaultDelete test helper

* prune/repair pack: document ignored errors

* selfupdate: improve error handling

* repository: show correct error if zstd decoder cannot be initialized
2026-06-20 17:09:02 +02:00
Michael Eischer 203e595ccb backend/sftp: fix removing files on windows sftp 2026-06-19 23:23:01 +02:00
Michael Eischer 6c67a095dc backend/sftp: fix error reporting of Save method 2026-06-19 23:09:59 +02:00
Michael Eischer 1a9decf03e repository: show correct error if zstd decoder cannot be initialized 2026-06-14 16:52:40 +02:00
Michael Eischer 718f09e02d selfupdate: improve error handling 2026-06-14 16:51:56 +02:00
Michael Eischer 82708ff7f7 prune/repair pack: document ignored errors 2026-06-14 16:51:56 +02:00
Michael Eischer 18c47c5bf9 backend/util: fix errorhandling in DefaultDelete test helper 2026-06-14 16:40:09 +02:00
Michael Eischer faea0d677a backend/sftp: fix file handle leak if chmod for a file fails 2026-06-14 16:39:42 +02:00
Michael Eischer ae8fdb08fa backend/rest: correctly close body on out of bounds read 2026-06-14 16:38:54 +02:00
Michael Eischer 5c8c048c1a backend/b2: correctly close file if too short 2026-06-14 16:38:21 +02:00
Michael Eischer 6b88b072e0 forget: fix error handling if snapshot key cannot be marshaled 2026-06-14 16:37:58 +02:00
Michael Eischer 7d36449ea8 repository: change LoadBlob, LookupBlob and LookupBlobSize to BlobHandles (#21857) 2026-06-14 15:30:00 +02:00
Michael Eischer 71f38d266c ensure reliable cleanup of test repository (#21880) 2026-06-14 14:25:00 +02:00
Michael Eischer 0536e15bbf snapshots: show location name instead of current timezone (#21877) 2026-06-14 14:02:20 +02:00
Michael Eischer 650f658244 repository/index: ensure progress bar cancelation on early index load failure (#21881) 2026-06-14 13:56:24 +02:00
Michael Eischer ae96b2a0f8 mount: fix crash in mountpoint validation (#21879) 2026-06-14 13:55:26 +02:00
Michael Eischer ce9b6ceea1 Merge pull request #21862 from MichaelEischer/lock-cleanup
Move Lock type to repository package and clean it up a bit
2026-06-13 21:46:26 +02:00
Michael Eischer 406dec29a7 repository: remove redundant mutex in checkForOtherLocks
forAllLocks already serializes the callback calls. Still has to create a
copy of checkedIDs to prevent a data race between forAllLocks and the
callback.
2026-06-13 21:37:24 +02:00
Michael Eischer 6fe10a72eb repository: drop mutex from lockHandle 2026-06-13 21:37:06 +02:00
Michael Eischer 1a6d43286a repository: comment cleanups 2026-06-13 21:36:36 +02:00
Michael Eischer 151903d4d4 repository: fix goroutine leak due to delayedCancelContext in tests 2026-06-13 21:35:51 +02:00
Michael Eischer b86568b790 repository: merge Unlocker and lockContext 2026-06-13 21:35:41 +02:00
Michael Eischer a5f3256626 repository: partially deduplicate refresh and refreshStaleLock 2026-06-13 21:33:58 +02:00
Michael Eischer 5989671f87 repository: deduplicate error handling in checkForOtherLocks 2026-06-13 21:33:43 +02:00
Michael Eischer b7f3a1367a repository: add SIGHUP handler only on Unix systems 2026-06-13 21:33:27 +02:00
Michael Eischer 4838b0ae3e repository: cleanup naming of lockCtx 2026-06-13 21:33:19 +02:00
Michael Eischer c87da70af9 repository: separate Lock in-repository from lock handle
Reduce Lock to a pure data transfer object and move the logic to
lockHandle.
2026-06-13 21:33:03 +02:00
Michael Eischer 81b6414c55 repository: hide Lock methods
Unexport lock file methods except String. Lock file operations are only
used within the repository package.
2026-06-13 21:32:13 +02:00
Michael Eischer 637c1cfb66 repository: unexport internal lock file helpers
Unexport UnlockCancelDelay, IsInvalidLock, ErrRemovedLock, NewLock,
StaleLockTimeout and ForAllLocks
2026-06-13 21:31:27 +02:00
Michael Eischer b892b1a150 repository: move lock file handling from restic package 2026-06-13 21:31:23 +02:00
Michael Eischer 7015da44ad restic: extract UidGidInt into separate files
Extract UidGidInt to simplify moving the locking code to the repository.
2026-06-13 21:24:38 +02:00
Michael Eischer 3ee465d363 repository: rename Lock function to LockRepo
Rename `Lock()` to `LockRepo()` to make room for the `Lock` struct.
2026-06-13 21:24:37 +02:00
Michael Eischer 8e11f5747d restic: introduce Counter interface to decouple from ui/progress (#21861)
decouple restic and ui/progress packages
2026-06-13 21:08:18 +02:00
Michael Eischer 22a304af7e Merge pull request #21849 from restic/internal-crypto-package
repository: move crypto package to internal/repository/crypto
2026-06-13 20:33:24 +02:00
Michael Eischer c8a0bc2c5e repository: move crypto package to internal/repository/crypto 2026-06-13 20:05:06 +02:00
Michael Eischer 35e60dc2c7 restic: remove Key() from repository interface
All callers use the concrete Repository type
2026-06-13 20:05:06 +02:00
Michael Eischer 8e93172bf8 cache: remove dependency on crypto package for truncation checks
The cache backend + repository can by now properly recover from damaged
files stored in the cache. Thus, remove the legacy sanity check.
2026-06-13 20:05:05 +02:00
Michael Eischer b94ef4831d repository: add PackBlob.UncompressedCiphertextLength() 2026-06-13 20:05:05 +02:00
Michael Eischer 952b310d62 Merge pull request #21851 from MichaelEischer/misc-cleanups2
Unexport lots of methods and structs
2026-06-13 20:03:18 +02:00
Michael Eischer 3eb5f83db3 fs: more vss related unexports 2026-06-13 19:54:32 +02:00
Michael Eischer d3ef21f870 archiver: unexport BackupTarget type 2026-06-13 19:54:12 +02:00
Michael Eischer 31df7508c4 archiver: unexport Options.ApplyDefaults 2026-06-13 19:53:51 +02:00
Michael Eischer 87c9ed4573 archiver: unexport toNoder 2026-06-13 19:53:31 +02:00
Michael Eischer eedba12968 bloblru: unexport add and get 2026-06-13 19:53:13 +02:00
Michael Eischer 264cfbcff2 data: remove unused Nodes type 2026-06-13 19:52:49 +02:00
Michael Eischer 7f3c9eb749 data: unexport fixTime 2026-06-13 19:52:27 +02:00
Michael Eischer 347cac780d data: unexport osType 2026-06-13 19:52:07 +02:00
Michael Eischer 23d6946fc9 fs: unexport CommandReader type 2026-06-13 19:51:49 +02:00
Michael Eischer 2db654533b fs: unexport Reader type 2026-06-13 19:51:33 +02:00
Michael Eischer f5843a7d97 fs: unexport more vss types 2026-06-13 19:51:14 +02:00
Michael Eischer e1e7fc8fb2 fs: unexport Local and expose via NewLocal 2026-06-13 19:50:49 +02:00
Michael Eischer 1acc19dac5 repository: unexport SearchKey 2026-06-13 19:49:03 +02:00
Michael Eischer 79e92b3bc6 repository: change Unlocker to interface 2026-06-13 19:48:47 +02:00
Michael Eischer 766d9f9f1e repository: unexport openKey 2026-06-13 19:48:31 +02:00
Michael Eischer 131abbb4f0 repository: unexport WarmupJob 2026-06-13 19:48:15 +02:00
Michael Eischer d62e42e620 repository: unexport NewChecker 2026-06-13 19:48:02 +02:00
Michael Eischer 35caceace4 restore: unexport HardlinkIndex.Index and hardlinkKey 2026-06-13 19:12:05 +02:00
Michael Eischer f2ff05f840 Merge pull request #21848 from restic/shrink-repository-interface
move restic.{Blob,Blobs,PackedBlob} types to `./internal/repository/pack` package
2026-06-13 19:11:33 +02:00
Michael Eischer c062a78dcd repository: move Blob, Blobs and PackedBlob to pack package
This removes them from the public interface. The latter now only
provides the PackBlob interface, without being bound to the type used
internally by the pack package.
2026-06-13 18:58:37 +02:00
Michael Eischer 35af104749 restic: change LookupBlob to return []PackBlob 2026-06-13 17:54:58 +02:00
Michael Eischer ccb5ae1592 restic: change ListBlobs to return PackBlob
PackBlob is a limited interface that only exposes a part of the
information provided by PackedBlob. Most of the changes are switches
from direct value lookups to the interface methods, with a few larger
changes to let the tests still work.
2026-06-13 17:54:38 +02:00
Michael Eischer 97f1e99ed9 restic: add PackBlob interface and implement it on PackedBlob
The PackBlob interface will allow hiding details from the public
interface, in particular, the offset of a blob within a pack file.
2026-06-13 17:54:18 +02:00
Michael Eischer c060c317d3 repository: unexport listPacksFromIndex
`ListPacksFromIndex` only has repository-internal callers left (besides
test code).
2026-06-13 17:54:03 +02:00
Michael Eischer a9e0b46358 restic: list pack header via ListPackHandles
Replace ListPack with ListPackHandles so callers only receive blob
handles from pack headers, not layout fields.
2026-06-13 17:50:21 +02:00
Michael Eischer 8169814b38 restic: switch LoadBlobsFromPack to BlobHandles
LoadBlobsFromPack now resolves the handles to Blobs. Repository internal
code can still use the Blob-based method.

The loader used in the filerestorer test now has to implement sorting
the blobs by offset itself as it no longer has access to the repository-internal
dataypes.
2026-06-13 17:50:21 +02:00
Michael Eischer f186e1e458 repository: require *Repository for CopyBlobs
Prepare `CopyBlobs` to allow access to unexported methods of the
Repository struct. This requires changing the test to inject the number
of backend connections via a wrapped backend instead of a wrapped
repository.
2026-06-13 17:50:21 +02:00
Winfried PlappertandMichael Eischer 0938f52f38 tests - more test cases for internal/repository/check.go (#21830)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-06-13 15:35:48 +00:00
Michael Eischer 9f9a3472aa Merge pull request #21850 from MichaelEischer/misc-cleanups
Cleanup `ui/*` package and minor cleanups in `test` and `walker`
2026-06-13 17:12:14 +02:00
Michael Eischer 14f86a462a stats: refactor ui progress printer into ui/stats 2026-06-13 17:00:11 +02:00
Michael Eischer 825d67ba4b backup,restore: move CalculateProgressInterval call to their ui package 2026-06-13 16:53:57 +02:00
Michael Eischer 3b8b77e54d test: remove unused exports 2026-06-13 16:53:57 +02:00
Michael Eischer 0bbfb072af ui/progress: unexport NoopPrinter and add New* function 2026-06-13 16:53:57 +02:00
Michael Eischer e8ed2434cd ui: move NewProgressPrinter to ui/progress.NewTerminalPrinter 2026-06-13 16:53:54 +02:00
Michael Eischer c04f3e9d6c ui: unexport json and text progress 2026-06-13 16:53:30 +02:00
Michael Eischer 37fdbe5779 ui: inline Message into progressPrinter 2026-06-13 16:53:30 +02:00
Michael Eischer 47328afb2e ui/progress: remove unused test helper 2026-06-13 16:53:30 +02:00
Michael Eischer e6d054d896 walker: remove empty testing.go 2026-06-13 16:53:30 +02:00
Michael Eischer fc81a0b6ad ui/terminal: unexport Terminal type 2026-06-13 16:53:30 +02:00
Michael Eischer dee28f5b0e ui/terminal: unexport New function 2026-06-13 16:53:30 +02:00
Michael Eischer 2d02a1ad39 doc: clarify forget --prune --json output (#21878) 2026-06-13 16:47:15 +02:00
Michael Eischer bae49d00ee stats: hide progress bar for json output (#21871) 2026-06-13 15:13:26 +02:00
e5dba15367 Json prune (#5239)
Co-authored-by: Alexander Weiss <alex@weissfam.de>
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-06-13 10:38:57 +00:00
Michael Eischer bf56d71b09 Merge pull request #21876 from MichaelEischer/cleanup-mount-validation
mount: refactor mountpoint validation
2026-06-12 23:34:44 +02:00
Michael Eischer e94ec65efb mount: unify mountpoint validation error messages 2026-06-12 22:30:53 +02:00
Michael Eischer 49f9e67520 mount: factor out mountpoint validation into helper 2026-06-12 22:29:45 +02:00
Michael Eischer 3a4b0e3b8c Merge pull request #5348 from zmanda/fix-gh-5234-prevent-mount-command-over-repository
mount: prevent mounting over repository
2026-06-12 22:27:50 +02:00
Michael Eischer abfc9bbdff Merge pull request #21868 from MichaelEischer/fix-zero-sized-blobs
repository: fix zero-sized blobs in v2 repos
2026-06-11 20:43:38 +02:00
Michael Eischer 784812361e Merge pull request #21858 from MichaelEischer/unexport-backends
Unexport several backend types
2026-06-10 22:51:32 +02:00
Michael Eischer c745f810b3 backend/cache: use backend types not restic types (#21860) 2026-06-10 22:45:39 +02:00
Michael Eischer 721411e04c backend/azure/gs/s3: remove unused Path() method 2026-06-10 22:37:07 +02:00
Michael Eischer b07ff6cd34 backends: remove unused SetListMax method
It was never used. Remove the dead code.
2026-06-10 22:37:07 +02:00
Michael Eischer 8b3bbb527c backend/s3: unexport 2026-06-10 22:37:07 +02:00
Michael Eischer 4a2d8e04aa backend/rclone: unexport 2026-06-10 22:37:07 +02:00
Michael Eischer a36488b706 backend/gs: unexport 2026-06-10 22:37:07 +02:00
Michael Eischer cfb506a77a backend/cache: unexport 2026-06-10 22:37:07 +02:00
Michael Eischer a15b1579ff Merge pull request #21846 from restic/repository-cleanups
Cleanup repository package
2026-06-10 22:36:04 +02:00
Michael Eischer ff95080f36 restic: move test helpers and drop unused JSONUnpackedLoader 2026-06-10 22:20:28 +02:00
Michael Eischer 119bb9d9a8 repository: require *Repository for ExaminePack
The function wouldn't work with a different Repository implementation
anyways.
2026-06-10 22:20:28 +02:00
Michael Eischer 8c6ee42d17 debug: move DumpPacks into repository package
Processing pack file internals belongs into the repository package.
2026-06-10 22:20:28 +02:00
Michael Eischer ccddc1914d repository: rename PackError to ErrPackMetadata 2026-06-10 22:20:28 +02:00
Michael Eischer 4bc5eca7ea repository: merge check.go into checker.go 2026-06-10 22:20:28 +02:00
Michael Eischer 17ff3aa5f9 Merge pull request #21841 from MichaelEischer/isolate-repository
prevent imports of repository internals
2026-06-10 22:19:24 +02:00
Michael Eischer 5683180a3b upgrade compress library 2026-06-10 22:03:56 +02:00
Michael Eischer 0fc7444e32 repository: fix zero-sized blobs in v2 repos 2026-06-10 22:03:40 +02:00
Michael Eischer 81571775d5 fix version set by helper after release (#21865) 2026-06-09 22:43:18 +02:00
Alexander Neumann 16b8b8cda0 Set development version for 0.19.0 2026-06-09 18:49:09 +02:00
Michael Eischer 7d25ca9d67 repository: omit unused headerSize from ListPack() 2026-05-31 22:53:00 +02:00
Michael Eischer cc546b71e3 debug: move code requring internal access to repository package 2026-05-31 22:48:38 +02:00
Michael Eischer 3cb49556f5 repair index: replace full index handling integration test with unit test 2026-05-31 22:39:16 +02:00
Michael Eischer f625190393 check: simplify index damage test 2026-05-31 22:19:15 +02:00
Michael Eischer 620f5986f8 list index: use helper in repository package 2026-05-31 22:14:01 +02:00
Michael Eischer 49c7364e79 add golangci-lint rule to prevent imports of repository internals 2026-05-31 18:20:37 +02:00
Srigovind Nayak e1e36ed848 doc: note that mountpoint must not overlap the repository
Document the new restriction added so users encountering the error
message have a reference, and so the constraint is visible before they
hit it.
2026-05-16 18:07:42 +05:30
Srigovind Nayak a37010a825 chore: update changelog for issue 5234 2026-05-16 18:06:14 +05:30
Srigovind Nayak a0d7745e8b mount: refuse mountpoints that overlap the local repository
Mounting a local repository onto its own directory caused the FUSE
  server to read its own backend files through the mount it had just
  created, deadlocking the kernel. `umount` then reported "Device or
  resource busy" and recovery required a reboot that took several
  minutes. The same shape occurs when the mountpoint is nested inside
  the repository directory, or when the repository directory is nested
  inside the mountpoint.

  The mount command now resolves both paths via filepath.Abs and
  filepath.EvalSymlinks and refuses with a fatal error if either path
  equals or contains the other. The check runs before the repository
  lock is acquired so an overlap fails fast. Only the local backend is
  checked; remote backends cannot shadow the mountpoint directory.
2026-05-16 18:03:49 +05:30
317 changed files with 5774 additions and 4257 deletions
+1 -1
View File
@@ -22,6 +22,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Codespell
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
+2 -2
View File
@@ -26,10 +26,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Log in to the Container registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
+13
View File
@@ -0,0 +1,13 @@
name: size-label
on: pull_request_target
jobs:
size-label:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: size-label
uses: "pascalgn/size-label-action@56b489b027932ec0cf60438a1a5f1a19c8fc71ff" # v0.5.7
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+8 -8
View File
@@ -57,10 +57,10 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go }}
@@ -222,10 +222,10 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go ${{ env.latest_go }}
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ env.latest_go }}
@@ -244,10 +244,10 @@ jobs:
checks: write
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go ${{ env.latest_go }}
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ env.latest_go }}
@@ -280,7 +280,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe
uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35
with:
jobs: ${{ toJSON(needs) }}
@@ -289,7 +289,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Docker meta
id: meta
+7 -1
View File
@@ -32,7 +32,6 @@ linters:
backend-imports:
files:
- "**/internal/backend/**"
- "!**/internal/backend/cache/**"
- "!**/internal/backend/test/**"
- "!**/*_test.go"
deny:
@@ -40,6 +39,13 @@ linters:
desc: "internal/restic should not be imported to keep the architectural layers intact"
- pkg: "github.com/restic/restic/internal/repository"
desc: "internal/repository should not be imported to keep the architectural layers intact"
repository-internals:
files:
- "**"
- "!**/internal/repository/**"
deny:
- pkg: "github.com/restic/restic/internal/repository/"
desc: "packages below internal/repository should not be imported to not depend on repository internals"
importas:
alias:
- pkg: github.com/restic/restic/internal/test
+1 -1
View File
@@ -93,7 +93,7 @@ release. Instructions on how to do that are contained in the
## News
You can follow the restic project on Mastodon [@resticbackup](https://fosstodon.org/@restic) or subscribe to
You can follow the restic project on Mastodon [@restic](https://fosstodon.org/@restic) or subscribe to
the [project blog](https://restic.net/blog/).
## License
+7
View File
@@ -0,0 +1,7 @@
Enhancement: show excluded items during backup in verbose mode
`restic backup -vv` now shows excluded items in verbose mode. Both text and JSON
output are supported.
https://github.com/restic/restic/issues/21870
https://github.com/restic/restic/pull/21887
+11
View File
@@ -0,0 +1,11 @@
Enhancement: `restic repair snapshots --forget` removes broken snapshots
In the past restic had no command to remove broken snapshot files.
`restic repair snapshots --forget` can now remove broken snapshot files,
which have been found previously by `restic check` or by earlier error messages from restic commands.
For example, `restic repair snapshots --forget 1d204771` will remove a broken snapshot file with id `1d204771`.
https://github.com/restic/restic/issues/21892
https://github.com/restic/restic/pull/21907
+7
View File
@@ -0,0 +1,7 @@
Enhancement: Add JSON support to prune
Restic `prune` now also supports the `--json` option and gives all
statistics in JSON format.
https://github.com/restic/restic/issues/3129
https://github.com/restic/restic/pull/5239
+8
View File
@@ -0,0 +1,8 @@
Enhancement: Add warmup support for check command on S3 backend
Like the other cold storage features, warmup support for the `check` command
remains experimental and gated behind the "s3-restore" feature flag.
https://github.com/restic/restic/pull/5248
https://github.com/restic/restic/issues/3202
https://github.com/restic/restic/issues/2504
+7
View File
@@ -0,0 +1,7 @@
Enhancement: Add command to list packfiles belonging to a snapshot
This enhancement adds command `list packs snapshotID` to show the packfiles
belonging to the given snapshot.
https://github.com/restic/restic/issues/5372
https://github.com/restic/restic/pull/5396
+7
View File
@@ -0,0 +1,7 @@
Enhancement: `restic repair packs` can handle missing packfiles
Missing and damaged packfiles previously required separate approaches to fix a repository.
Now, both cases can be handled by the `repair packs` command, which is now able to handle
missing packfiles by removing them from the repository index.
https://github.com/restic/restic/pull/21845
+6
View File
@@ -0,0 +1,6 @@
Enhancement: `find --pack` can handle corrupted pack files
`restic find --pack <id>` can now also report affected snapshot if a packfile
is corrupted but still present in the repository index.
https://github.com/restic/restic/pull/21883
+8
View File
@@ -0,0 +1,8 @@
Enhancement: Reduce memory usage of the in-memory index
Restic keeps an index of the repository contents in memory during most
operations. This index now requires roughly 12% less memory, which is
especially noticeable for large repositories with many snapshots, where
a lot of index data has to be loaded.
https://github.com/restic/restic/pull/21937
+9
View File
@@ -0,0 +1,9 @@
Bugfix: Report an error instead of crashing on a truncated key or config file
Restic crashed with a slice bounds panic when a repository contained a key
file or a config file that was shorter than the encryption overhead, for
example after an upload was interrupted. Since every command has to read a
key file, the whole repository became unusable until the file was removed.
Restic now reports that the file is too short instead.
https://github.com/restic/restic/pull/21977
+7
View File
@@ -0,0 +1,7 @@
Bugfix: Redact Swift backend password in debug log output
Restic could write the OpenStack Swift backend password (OS_PASSWORD or
ST_KEY) in clear text to the debug log when debug logging was enabled.
The password is now redacted.
https://github.com/restic/restic/pull/22007
+37 -30
View File
@@ -10,6 +10,7 @@ import (
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"time"
@@ -53,22 +54,7 @@ Exit status is 11 if the repository is already locked.
Exit status is 12 if the password is incorrect.
`,
PreRunE: func(_ *cobra.Command, _ []string) error {
if envVal := os.Getenv("RESTIC_READ_CONCURRENCY"); envVal != "" && !opts.readConcurrencyFlag.Changed {
n, err := strconv.ParseUint(envVal, 10, 32)
if err != nil {
return errors.Fatalf("invalid value for RESTIC_READ_CONCURRENCY %q: %v", envVal, err)
}
opts.ReadConcurrency = uint(n)
}
if opts.Host == "" {
hostname, err := os.Hostname()
if err != nil {
debug.Log("os.Hostname() returned err: %v", err)
return nil
}
opts.Host = hostname
}
return nil
return opts.Finalize()
},
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
@@ -143,8 +129,8 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
f.StringArrayVar(&opts.FilesFromRaw, "files-from-raw", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
f.StringVar(&opts.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
f.BoolVar(&opts.WithAtime, "with-atime", false, "store the atime for all files and directories")
f.BoolVar(&opts.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files")
f.BoolVar(&opts.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files")
f.BoolVar(&opts.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files (default: $RESTIC_IGNORE_INODE or false)")
f.BoolVar(&opts.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files (default: $RESTIC_IGNORE_CTIME or false)")
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not upload or write any data, just show what would be done")
f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
if runtime.GOOS == "windows" {
@@ -157,12 +143,35 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
opts.readConcurrencyFlag = f.Lookup("read-concurrency")
// parse read inode and ctime from env, on error the default value will be used
opts.IgnoreInode, _ = strconv.ParseBool(os.Getenv("RESTIC_IGNORE_INODE"))
opts.IgnoreCtime, _ = strconv.ParseBool(os.Getenv("RESTIC_IGNORE_CTIME"))
// parse host from env, if not exists or empty the default value will be used
if host := os.Getenv("RESTIC_HOST"); host != "" {
opts.Host = host
}
}
func (opts *BackupOptions) Finalize() error {
if envVal := os.Getenv("RESTIC_READ_CONCURRENCY"); envVal != "" && !opts.readConcurrencyFlag.Changed {
n, err := strconv.ParseUint(envVal, 10, 32)
if err != nil {
return errors.Fatalf("invalid value for RESTIC_READ_CONCURRENCY %q: %v", envVal, err)
}
opts.ReadConcurrency = uint(n)
}
if opts.Host == "" {
hostname, err := os.Hostname()
if err != nil {
debug.Log("os.Hostname() returned err: %v", err)
return nil
}
opts.Host = hostname
}
return nil
}
var backupFSTestHook func(fs fs.FS) fs.FS
// ErrInvalidSourceData is used to report an incomplete backup
@@ -173,7 +182,7 @@ var ErrNoSourceData = errors.Fatal("all source directories/files do not exist")
// filterExisting returns the items that exist and can be accessed. It returns
// ErrNoSourceData if none remain, or ErrInvalidSourceData if some were skipped.
func filterExisting(items []string, warnf func(msg string, args ...interface{})) (result []string, err error) {
func filterExisting(items []string, warnf func(msg string, args ...any)) (result []string, err error) {
for _, item := range items {
_, err := fs.Lstat(item)
if err != nil {
@@ -296,10 +305,8 @@ func (opts BackupOptions) Check(gopts global.Options, args []string) error {
}
filesFrom := append(append(opts.FilesFrom, opts.FilesFromVerbatim...), opts.FilesFromRaw...)
for _, filename := range filesFrom {
if filename == "-" {
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
}
if slices.Contains(filesFrom, "-") {
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
}
}
@@ -324,7 +331,7 @@ func (opts BackupOptions) Check(gopts global.Options, args []string) error {
// collectRejectByNameFuncs returns a list of all functions which may reject data
// from being saved in a snapshot based on path only
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...interface{})) (fs []archiver.RejectByNameFunc, err error) {
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...any)) (fs []archiver.RejectByNameFunc, err error) {
// exclude restic cache
if repo.Cache() != nil {
f, err := rejectResticCache(repo)
@@ -348,7 +355,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, w
// collectRejectFuncs returns a list of all functions which may reject data
// from being saved in a snapshot based on path and file info
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...interface{})) (funcs []archiver.RejectFunc, err error) {
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...any)) (funcs []archiver.RejectFunc, err error) {
// allowed devices
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
f, err := archiver.RejectByDevice(targets, fs)
@@ -396,7 +403,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf fu
}
// collectTargets returns a list of target files/dirs from several sources.
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{}), stdin io.ReadCloser) (targets []string, err error) {
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...any), stdin io.ReadCloser) (targets []string, err error) {
if opts.Stdin || opts.StdinCommand {
return nil, nil
}
@@ -537,8 +544,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
}
defer unlock()
progressReporter := backup.NewProgress(printer,
ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
progressReporter := backup.NewProgress(printer, gopts.Quiet, gopts.JSON, term.CanUpdateStatus())
defer progressReporter.Done()
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
@@ -572,7 +578,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
return err
}
var targetFS fs.FS = fs.Local{}
targetFS := fs.NewLocal()
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
return err
@@ -582,7 +588,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
_ = progressReporter.Error(item, err)
}
messageHandler := func(msg string, args ...interface{}) {
messageHandler := func(msg string, args ...any) {
if !gopts.JSON {
printer.P(msg, args...)
}
@@ -663,6 +669,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
arch.CompleteItem = progressReporter.CompleteItem
arch.StartFile = progressReporter.StartFile
arch.CompleteBlob = progressReporter.CompleteBlob
arch.ExcludedItem = progressReporter.ExcludedItem
if opts.IgnoreInode {
// --ignore-inode implies --ignore-ctime: on FUSE, the ctime is not
+56 -9
View File
@@ -1,11 +1,15 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
"time"
@@ -13,8 +17,10 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/backup"
)
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) error {
@@ -30,6 +36,13 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts
})
}
func testRunBackupOutput(t testing.TB, opts BackupOptions, gopts global.Options, target []string) ([]byte, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runBackup(ctx, opts, gopts, gopts.Term, target)
})
return buf.Bytes(), err
}
func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) {
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
rtest.Assert(t, err == nil, "Error while backing up: %v", err)
@@ -49,6 +62,9 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
// Use auto compression to ensure coverage in the integration tests
// All other tests use fastest compression for faster execution
env.gopts.Compression = repository.CompressionAuto
testSetupBackupData(t, env)
opts := BackupOptions{UseFsSnapshot: useFsSnapshot}
@@ -139,7 +155,7 @@ func (f *vssDeleteOriginalFS) Lstat(name string) (*fs.ExtendedFileInfo, error) {
_, _ = f.FS.Lstat(name)
// nuke testdata
var err error
for i := 0; i < 3; i++ {
for range 3 {
// The CI sometimes runs into "The process cannot access the file because it is being used by another process" errors
// thus try a few times to remove the data
err = os.RemoveAll(f.testdata)
@@ -615,13 +631,7 @@ func linkEqual(source, dest []string) bool {
}
for i := range source {
found := false
for j := range dest {
if source[i] == dest[j] {
found = true
break
}
}
found := slices.Contains(dest, source[i])
if !found {
return false
}
@@ -723,10 +733,47 @@ func TestBackupSkipIfUnchanged(t *testing.T) {
testSetupBackupData(t, env)
opts := BackupOptions{SkipIfUnchanged: true}
for i := 0; i < 3; i++ {
for range 3 {
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
testListSnapshots(t, env.gopts, 1)
}
testRunCheck(t, env.gopts)
}
func TestBackupExcludeWithOutput(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
backupOptions := BackupOptions{}
backupOptions.Excludes = []string{"*.py"}
env.gopts.JSON = true
env.gopts.Verbosity = 2
output, err := testRunBackupOutput(t, backupOptions, env.gopts, []string{filepath.Join(env.testdata, "0", "for_cmd_ls")})
rtest.OK(t, err)
foundExclude := false
for line := range bytes.SplitSeq(output, []byte("\n")) {
if len(line) == 0 {
continue
}
type MessageType struct {
MessageType string `json:"message_type"` // any
}
var mType MessageType
rtest.OK(t, json.Unmarshal(line, &mType))
if mType.MessageType != "excluded_item" {
continue
}
var excludeLine backup.VerboseExclude
rtest.OK(t, json.Unmarshal(line, &excludeLine))
rtest.Assert(t, strings.Contains(excludeLine.Item, ".py"), "expected excluded pathname to be ending in .py, but contains %q",
excludeLine.Item)
foundExclude = true
}
rtest.Assert(t, foundExclude, "expected at least one excluded item, but found none")
}
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -57,7 +58,7 @@ func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
}
func runCache(opts CacheOptions, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
if len(args) > 0 {
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
+8 -12
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"slices"
"strings"
"github.com/spf13/cobra"
@@ -13,6 +14,7 @@ import (
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
@@ -48,13 +50,7 @@ func validateCatArgs(args []string) error {
return errors.Fatal("type not specified")
}
validType := false
for _, v := range catAllowedCmds {
if v == args[0] {
validType = true
break
}
}
validType := slices.Contains(catAllowedCmds, args[0])
if !validType {
return errors.Fatalf("invalid type %q, must be one of [%s]", args[0], strings.Join(catAllowedCmds, "|"))
}
@@ -67,7 +63,7 @@ func validateCatArgs(args []string) error {
}
func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
if err := validateCatArgs(args); err != nil {
return err
@@ -141,7 +137,7 @@ func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Te
printer.S(string(buf))
return nil
case "lock":
lock, err := restic.LoadLock(ctx, repo, id)
lock, err := repository.LoadLock(ctx, repo, id)
if err != nil {
return err
}
@@ -176,11 +172,11 @@ func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Te
}
for _, t := range []restic.BlobType{restic.DataBlob, restic.TreeBlob} {
if _, ok := repo.LookupBlobSize(t, id); !ok {
if _, ok := repo.LookupBlobSize(restic.BlobHandle{Type: t, ID: id}); !ok {
continue
}
buf, err := repo.LoadBlob(ctx, t, id, nil)
buf, err := repo.LoadBlob(ctx, restic.BlobHandle{Type: t, ID: id}, nil)
if err != nil {
return err
}
@@ -207,7 +203,7 @@ func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Te
return err
}
buf, err := repo.LoadBlob(ctx, restic.TreeBlob, *sn.Tree, nil)
buf, err := repo.LoadBlob(ctx, restic.BlobHandle{Type: restic.TreeBlob, ID: *sn.Tree}, nil)
if err != nil {
return err
}
+33 -26
View File
@@ -179,7 +179,7 @@ func parsePercentage(s string) (float64, error) {
// - if the user explicitly requested --no-cache, we don't use any cache
// - if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check
// - by default, we use a cache in a temporary directory that is deleted after the check
func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progress.Printer) (cleanup func()) {
func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer restic.Printer) (cleanup func()) {
cleanup = func() {}
if opts.WithCache {
// use the default cache, no setup needed
@@ -229,9 +229,9 @@ func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progres
func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args []string, term ui.Terminal) (checkSummary, error) {
summary := checkSummary{MessageType: "summary"}
var printer progress.Printer
var printer restic.Printer
if !gopts.JSON {
printer = ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer = progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
} else {
printer = newJSONErrorPrinter(term)
}
@@ -307,13 +307,13 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
go chkr.Packs(ctx, errChan)
for err := range errChan {
var packErr *repository.PackError
var packErr *repository.ErrPackMetadata
if errors.As(err, &packErr) {
if packErr.Orphaned {
orphanedPacks++
printer.V("%v\n", err)
} else {
if packErr.Truncated {
if packErr.Truncated || packErr.Missing {
salvagePacks.Insert(packErr.ID)
}
errorsFound = true
@@ -339,25 +339,28 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
printer.P("check snapshots, trees and blobs\n")
errChan = make(chan error)
var brokenSnapshots []string
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
bar := printer.NewCounter("snapshots")
defer bar.Done()
chkr.Structure(ctx, bar, errChan)
}()
})
for err := range errChan {
errorsFound = true
if e, ok := err.(*checker.TreeError); ok {
switch e := err.(type) {
case *checker.TreeError:
printer.E("error for tree %v:\n", e.ID.Str())
for _, treeErr := range e.Errors {
summary.NumErrors++
printer.E(" %v\n", treeErr)
}
} else {
case *checker.SnapshotError:
printer.E("snapshot error %v: %v", e.ID, e.Message)
brokenSnapshots = append(brokenSnapshots, e.ID)
default:
summary.NumErrors++
printer.E("error: %v\n", err)
}
@@ -389,10 +392,9 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
}
if readDataFilter != nil {
p := printer.NewCounter("packs")
errChan := make(chan error)
go chkr.ReadPacks(ctx, readDataFilter, p, errChan)
go chkr.ReadPacks(ctx, readDataFilter, printer, errChan)
for err := range errChan {
errorsFound = true
@@ -402,7 +404,6 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
salvagePacks.Insert(err.PackID)
}
}
p.Done()
}
if len(salvagePacks) > 0 {
@@ -414,12 +415,18 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
printer.E("Damaged pack files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!\n")
}
if len(brokenSnapshots) > 0 {
printer.E("\nThe repository contains damaged snapshot files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.\n\n")
printer.E("restic repair snapshots --forget %s\n\n", strings.Join(brokenSnapshots, " "))
printer.E("Damaged snapshot files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!\n")
}
if ctx.Err() != nil {
return summary, ctx.Err()
}
if errorsFound {
if len(salvagePacks) == 0 {
if len(salvagePacks) == 0 && len(brokenSnapshots) == 0 {
printer.E("\nThe repository is damaged and must be repaired. Please follow the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html .\n\n")
}
return summary, errors.Fatal("repository contains errors")
@@ -428,7 +435,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
return summary, nil
}
func buildPacksFilter(opts CheckOptions, printer progress.Printer,
func buildPacksFilter(opts CheckOptions, printer restic.Printer,
filteredStatus bool) (func(packs map[restic.ID]int64) map[restic.ID]int64, error) {
typeData := ""
if filteredStatus {
@@ -553,23 +560,23 @@ func newJSONErrorPrinter(term ui.Terminal) *jsonErrorPrinter {
}
}
func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter {
return nil
func (*jsonErrorPrinter) NewCounter(_ string) restic.Counter {
return restic.NoopCounter
}
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) *progress.Counter {
return nil
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) restic.Counter {
return restic.NoopCounter
}
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
func (p *jsonErrorPrinter) E(msg string, args ...any) {
status := checkError{
MessageType: "error",
Message: fmt.Sprintf(msg, args...),
}
p.term.Error(ui.ToJSONString(status))
}
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) S(_ string, _ ...any) {}
func (*jsonErrorPrinter) P(_ string, _ ...any) {}
func (*jsonErrorPrinter) PT(_ string, _ ...any) {}
func (*jsonErrorPrinter) V(_ string, _ ...any) {}
func (*jsonErrorPrinter) VV(_ string, _ ...any) {}
+9 -8
View File
@@ -11,35 +11,36 @@ import (
func testRunCheck(t testing.TB, gopts global.Options) {
t.Helper()
output, err := testRunCheckOutput(t, gopts, true)
stdout, stderr, err := testRunCheckOutput(t, gopts, true)
if err != nil {
t.Error(output)
t.Error(stdout)
t.Error(stderr)
t.Fatalf("unexpected error: %+v", err)
}
}
func testRunCheckMustFail(t testing.TB, gopts global.Options) {
t.Helper()
_, err := testRunCheckOutput(t, gopts, false)
_, _, err := testRunCheckOutput(t, gopts, false)
rtest.Assert(t, err != nil, "expected non nil error after check of damaged repository")
}
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, string, error) {
stdout, stderr, err := withCaptureStdoutStderr(t, gopts, func(ctx context.Context, gopts global.Options) error {
opts := CheckOptions{
ReadData: true,
CheckUnused: checkUnused,
}
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.Term)
_, err := runCheck(ctx, opts, gopts, nil, gopts.Term)
return err
})
return buf.String(), err
return stdout.String(), stderr.String(), err
}
func testRunCheckOutputWithOpts(t testing.TB, gopts global.Options, opts CheckOptions, args []string) (string, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
gopts.Verbosity = 2
_, err := runCheck(context.TODO(), opts, gopts, args, gopts.Term)
_, err := runCheck(ctx, opts, gopts, args, gopts.Term)
return err
})
return buf.String(), err
+2 -3
View File
@@ -12,7 +12,6 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/progress"
)
func TestParsePercentage(t *testing.T) {
@@ -204,7 +203,7 @@ func TestPrepareCheckCache(t *testing.T) {
rtest.OK(t, err)
}
gopts := global.Options{CacheDir: tmpDirBase}
cleanup := prepareCheckCache(testCase.opts, &gopts, &progress.NoopPrinter{})
cleanup := prepareCheckCache(testCase.opts, &gopts, restic.NewNoopPrinter())
files, err := os.ReadDir(tmpDirBase)
rtest.OK(t, err)
@@ -234,7 +233,7 @@ func TestPrepareCheckCache(t *testing.T) {
func TestPrepareDefaultCheckCache(t *testing.T) {
gopts := global.Options{CacheDir: ""}
cleanup := prepareCheckCache(CheckOptions{}, &gopts, &progress.NoopPrinter{})
cleanup := prepareCheckCache(CheckOptions{}, &gopts, restic.NewNoopPrinter())
_, err := os.ReadDir(gopts.CacheDir)
rtest.OK(t, err)
+45 -26
View File
@@ -72,14 +72,22 @@ func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
}
var errSentinelEndIteration = errors.New("end iteration")
// collectAllSnapshots: select all snapshot trees to be copied
func collectAllSnapshots(ctx context.Context, opts CopyOptions,
srcSnapshotLister restic.Lister, srcRepo restic.Repository,
dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer progress.Printer,
) iter.Seq[*data.Snapshot] {
return func(yield func(*data.Snapshot) bool) {
for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args, printer) {
dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer restic.Printer,
) iter.Seq2[*data.Snapshot, error] {
return func(yield func(*data.Snapshot, error) bool) {
err := opts.SnapshotFilter.FindAll(ctx, srcSnapshotLister, srcRepo, args, func(_ string, sn *data.Snapshot, err error) error {
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
if err != nil {
if !yield(nil, err) {
return errSentinelEndIteration
}
return nil
}
srcOriginal := *sn.ID()
if sn.Original != nil {
srcOriginal = *sn.Original
@@ -95,18 +103,22 @@ func collectAllSnapshots(ctx context.Context, opts CopyOptions,
}
}
if isCopy {
continue
return nil
}
}
if !yield(sn) {
return
if !yield(sn, nil) {
return errSentinelEndIteration
}
return nil
})
if err != nil && !errors.Is(err, errSentinelEndIteration) {
yield(nil, err)
}
}
}
func runCopy(ctx context.Context, opts CopyOptions, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
secondaryGopts, isFromRepo, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "destination")
if err != nil {
return err
@@ -148,15 +160,19 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts global.Options, args [
}
dstSnapshotByOriginal := make(map[restic.ID][]*data.Snapshot)
for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil, printer) {
err = opts.SnapshotFilter.FindAll(ctx, dstSnapshotLister, dstRepo, nil, func(_ string, sn *data.Snapshot, err error) error {
if err != nil {
return err
}
if sn.Original != nil && !sn.Original.IsNull() {
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
}
// also consider identical snapshot copies
dstSnapshotByOriginal[*sn.ID()] = append(dstSnapshotByOriginal[*sn.ID()], sn)
}
if ctx.Err() != nil {
return ctx.Err()
return nil
})
if err != nil {
return err
}
selectedSnapshots := collectAllSnapshots(ctx, opts, srcSnapshotLister, srcRepo, dstSnapshotByOriginal, args, printer)
@@ -189,8 +205,8 @@ func similarSnapshots(sna *data.Snapshot, snb *data.Snapshot) bool {
// copyTreeBatched copies multiple snapshots in one go. Snapshots are written after
// data equivalent to at least 10 packfiles was written.
func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
selectedSnapshots iter.Seq[*data.Snapshot], printer progress.Printer) error {
func copyTreeBatched(ctx context.Context, srcRepo *repository.Repository, dstRepo restic.Repository,
selectedSnapshots iter.Seq2[*data.Snapshot, error], printer restic.Printer) error {
// remember already processed trees across all snapshots
visitedTrees := srcRepo.NewAssociatedBlobSet()
@@ -199,7 +215,7 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
minDuration := 1 * time.Minute
// use pull-based iterator to allow iteration in multiple steps
next, stop := iter.Pull(selectedSnapshots)
next, stop := iter.Pull2(selectedSnapshots)
defer stop()
for {
@@ -210,7 +226,10 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
// call WithBlobUploader() once and then loop over all selectedSnapshots
err := dstRepo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
for batchSize < targetSize || time.Since(startTime) < minDuration {
sn, ok := next()
sn, err, ok := next()
if err != nil {
return err
}
if !ok {
break
}
@@ -254,8 +273,8 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
return nil
}
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer progress.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
func copyTree(ctx context.Context, srcRepo *repository.Repository, dstRepo restic.Repository,
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer restic.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
copyBlobs := srcRepo.NewAssociatedBlobSet()
packList := restic.NewIDSet()
@@ -264,16 +283,16 @@ func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Rep
enqueue := func(h restic.BlobHandle) {
lock.Lock()
defer lock.Unlock()
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
pb := srcRepo.LookupBlob(h.Type, h.ID)
if _, ok := dstRepo.LookupBlobSize(h); !ok {
pb := srcRepo.LookupBlob(h)
copyBlobs.Insert(h)
for _, p := range pb {
packList.Insert(p.PackID)
packList.Insert(p.PackID())
}
}
}
err := data.StreamTrees(ctx, srcRepo, restic.IDs{rootTreeID}, nil, func(treeID restic.ID) bool {
err := data.StreamTrees(ctx, srcRepo, restic.IDs{rootTreeID}, restic.NoopCounter, func(treeID restic.ID) bool {
handle := restic.BlobHandle{ID: treeID, Type: restic.TreeBlob}
visited := visitedTrees.Has(handle)
visitedTrees.Insert(handle)
@@ -312,14 +331,14 @@ func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Rep
}
// copyStats: print statistics for the blobs to be copied
func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, packList restic.IDSet, printer progress.Printer) uint64 {
func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, packList restic.IDSet, printer restic.Printer) uint64 {
// count and size
countBlobs := 0
sizeBlobs := uint64(0)
for blob := range copyBlobs.Keys() {
for _, blob := range srcRepo.LookupBlob(blob.Type, blob.ID) {
for _, pb := range srcRepo.LookupBlob(blob) {
countBlobs++
sizeBlobs += uint64(blob.Length)
sizeBlobs += uint64(pb.CiphertextLength())
break
}
}
@@ -329,7 +348,7 @@ func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, pa
return sizeBlobs
}
func copySaveSnapshot(ctx context.Context, sn *data.Snapshot, dstRepo restic.Repository, printer progress.Printer) error {
func copySaveSnapshot(ctx context.Context, sn *data.Snapshot, dstRepo restic.Repository, printer restic.Printer) error {
sn.Parent = nil // Parent does not have relevance in the new repo.
// Use Original as a persistent snapshot ID
if sn.Original == nil {
+6 -6
View File
@@ -9,7 +9,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
func testRunCopy(t testing.TB, srcGopts global.Options, dstGopts global.Options) {
@@ -97,23 +97,23 @@ func TestCopy(t *testing.T) {
func testPackAndBlobCounts(t testing.TB, gopts global.Options) (countTreePacks int, countDataPacks int, countBlobs int) {
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
rtest.OK(t, repo.List(context.TODO(), restic.PackFile, func(id restic.ID, size int64) error {
blobs, _, err := repo.ListPack(context.TODO(), id, size)
handles, err := repo.ListPackHandles(context.TODO(), id, size)
rtest.OK(t, err)
rtest.Assert(t, len(blobs) > 0, "a packfile should contain at least one blob")
rtest.Assert(t, len(handles) > 0, "a packfile should contain at least one blob")
switch blobs[0].Type {
switch handles[0].Type {
case restic.TreeBlob:
countTreePacks++
case restic.DataBlob:
countDataPacks++
}
countBlobs += len(blobs)
countBlobs += len(handles)
return nil
}))
return nil
+12 -366
View File
@@ -4,28 +4,17 @@ package main
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/json"
"fmt"
"io"
"os"
"runtime"
"sync"
"time"
"github.com/klauspost/compress/zstd"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
@@ -128,63 +117,8 @@ func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io
})
}
// Pack is the struct used in printPacks.
type Pack struct {
Name string `json:"name"`
Blobs []Blob `json:"blobs"`
}
// Blob is the struct used in printPacks.
type Blob struct {
Type restic.BlobType `json:"type"`
Length uint `json:"length"`
ID restic.ID `json:"id"`
Offset uint `json:"offset"`
}
func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer, printer progress.Printer) error {
var m sync.Mutex
return restic.ParallelList(ctx, repo, restic.PackFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error {
blobs, _, err := repo.ListPack(ctx, id, size)
if err != nil {
printer.E("error for pack %v: %v", id.Str(), err)
return nil
}
p := Pack{
Name: id.String(),
Blobs: make([]Blob, len(blobs)),
}
for i, blob := range blobs {
p.Blobs[i] = Blob{
Type: blob.Type,
Length: blob.Length,
ID: blob.ID,
Offset: blob.Offset,
}
}
m.Lock()
defer m.Unlock()
return prettyPrintJSON(wr, p)
})
}
func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer, printer progress.Printer) error {
return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error {
printer.S("index_id: %v", id)
if err != nil {
return err
}
return idx.Dump(wr)
})
}
func runDebugDump(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
if len(args) != 1 {
return errors.Fatal("type not specified")
@@ -200,11 +134,11 @@ func runDebugDump(ctx context.Context, gopts global.Options, args []string, term
switch tpe {
case "indexes":
return dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
return repository.DumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
case "snapshots":
return debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
case "packs":
return printPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
return repository.DumpPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
case "all":
printer.S("snapshots:")
err := debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
@@ -213,7 +147,7 @@ func runDebugDump(ctx context.Context, gopts global.Options, args []string, term
}
printer.S("indexes:")
err = dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
err = repository.DumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
if err != nil {
return err
}
@@ -224,227 +158,8 @@ func runDebugDump(ctx context.Context, gopts global.Options, args []string, term
}
}
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer progress.Printer) []byte {
if bytewise {
printer.S(" trying to repair blob by finding a broken byte")
} else {
printer.S(" trying to repair blob with single bit flip")
}
ch := make(chan int)
var wg errgroup.Group
done := make(chan struct{})
var fixed []byte
var found bool
workers := runtime.GOMAXPROCS(0)
printer.S(" spinning up %d worker functions", runtime.GOMAXPROCS(0))
for i := 0; i < workers; i++ {
wg.Go(func() error {
// make a local copy of the buffer
buf := make([]byte, len(input))
copy(buf, input)
testFlip := func(idx int, pattern byte) bool {
// flip bits
buf[idx] ^= pattern
nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
plaintext, err := key.Open(plaintext[:0], nonce, plaintext, nil)
if err == nil {
printer.S("")
printer.S(" blob could be repaired by XORing byte %v with 0x%02x", idx, pattern)
printer.S(" hash is %v", restic.Hash(plaintext))
close(done)
found = true
fixed = plaintext
return true
}
// flip bits back
buf[idx] ^= pattern
return false
}
for i := range ch {
if bytewise {
for j := 0; j < 255; j++ {
if testFlip(i, byte(j)) {
return nil
}
}
} else {
for j := 0; j < 7; j++ {
// flip each bit once
if testFlip(i, (1 << uint(j))) {
return nil
}
}
}
}
return nil
})
}
wg.Go(func() error {
defer close(ch)
start := time.Now()
info := time.Now()
for i := range input {
select {
case ch <- i:
case <-done:
printer.S(" done after %v", time.Since(start))
return nil
}
if time.Since(info) > time.Second {
secs := time.Since(start).Seconds()
gps := float64(i) / secs
remaining := len(input) - i
eta := time.Duration(float64(remaining)/gps) * time.Second
printer.S("\r%d byte of %d done (%.2f%%), %.0f byte per second, ETA %v",
i, len(input), float32(i)/float32(len(input))*100, gps, eta)
info = time.Now()
}
}
return nil
})
err := wg.Wait()
if err != nil {
panic("all go routines can only return nil")
}
if !found {
printer.S("\n blob could not be repaired")
}
return fixed
}
func decryptUnsigned(k *crypto.Key, buf []byte) []byte {
// strip signature at the end
l := len(buf)
nonce, ct := buf[:16], buf[16:l-16]
out := make([]byte, len(ct))
c, err := aes.NewCipher(k.EncryptionKey[:])
if err != nil {
panic(fmt.Sprintf("unable to create cipher: %v", err))
}
e := cipher.NewCTR(c, nonce)
e.XORKeyStream(out, ct)
return out
}
func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, packID restic.ID, list restic.Blobs, printer progress.Printer) error {
dec, err := zstd.NewReader(nil)
if err != nil {
panic(err)
}
pack, err := repo.LoadRaw(ctx, restic.PackFile, packID)
// allow processing broken pack files
if pack == nil {
return err
}
err = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
for _, blob := range list {
printer.S(" loading blob %v at %v (length %v)", blob.ID, blob.Offset, blob.Length)
if int(blob.Offset+blob.Length) > len(pack) {
printer.E("skipping truncated blob")
continue
}
buf := pack[blob.Offset : blob.Offset+blob.Length]
key := repo.Key()
nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
plaintext, err = key.Open(plaintext[:0], nonce, plaintext, nil)
outputPrefix := ""
filePrefix := ""
if err != nil {
printer.E("error decrypting blob: %v", err)
if opts.TryRepair || opts.RepairByte {
plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte, printer)
}
if plaintext != nil {
outputPrefix = "repaired "
filePrefix = "repaired-"
} else {
plaintext = decryptUnsigned(key, buf)
err = storePlainBlob(blob.ID, "damaged-", plaintext, printer)
if err != nil {
return err
}
continue
}
}
if blob.IsCompressed() {
decompressed, err := dec.DecodeAll(plaintext, nil)
if err != nil {
printer.S(" failed to decompress blob %v", blob.ID)
}
if decompressed != nil {
plaintext = decompressed
}
}
id := restic.Hash(plaintext)
var prefix string
if !id.Equal(blob.ID) {
printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID does not match, wanted %v", outputPrefix, len(plaintext), id, blob.ID)
prefix = "wrong-hash-"
} else {
printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID matches", outputPrefix, len(plaintext), id)
prefix = "correct-"
}
if opts.ExtractPack {
err = storePlainBlob(id, filePrefix+prefix, plaintext, printer)
if err != nil {
return err
}
}
if opts.ReuploadBlobs {
_, _, _, err := uploader.SaveBlob(ctx, blob.Type, plaintext, id, true)
if err != nil {
return err
}
printer.S(" uploaded %v %v", blob.Type, id)
}
}
return nil
})
return err
}
func storePlainBlob(id restic.ID, prefix string, plain []byte, printer progress.Printer) error {
filename := fmt.Sprintf("%s%s.bin", prefix, id)
f, err := os.Create(filename)
if err != nil {
return err
}
_, err = f.Write(plain)
if err != nil {
_ = f.Close()
return err
}
err = f.Close()
if err != nil {
return err
}
printer.S("decrypt of blob %v stored at %v", id, filename)
return nil
}
func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamineOptions, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
if opts.ExtractPack && gopts.NoLock {
return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive")
@@ -478,8 +193,14 @@ func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamin
return err
}
examineOpts := repository.ExaminePackOptions{
TryRepair: opts.TryRepair,
RepairByte: opts.RepairByte,
ExtractPack: opts.ExtractPack,
ReuploadBlobs: opts.ReuploadBlobs,
}
for _, id := range ids {
err := examinePack(ctx, opts, repo, id, printer)
err := repository.ExaminePack(ctx, repo, id, examineOpts, printer)
if err != nil {
printer.E("error: %v", err)
}
@@ -489,78 +210,3 @@ func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamin
}
return nil
}
func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID, printer progress.Printer) error {
printer.S("examine %v", id)
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
// also process damaged pack files
if buf == nil {
return err
}
printer.S(" file size is %v", len(buf))
gotID := restic.Hash(buf)
if !id.Equal(gotID) {
printer.S(" wanted hash %v, got %v", id, gotID)
} else {
printer.S(" hash for file content matches")
}
printer.S(" ========================================")
printer.S(" looking for info in the indexes")
blobsLoaded := false
// examine all data the indexes have for the pack file
for b := range repo.ListPacksFromIndex(ctx, restic.NewIDSet(id)) {
blobs := b.Blobs
if len(blobs) == 0 {
continue
}
checkPackSize(blobs, len(buf), printer)
err = loadBlobs(ctx, opts, repo, id, blobs, printer)
if err != nil {
printer.E("error: %v", err)
} else {
blobsLoaded = true
}
}
printer.S(" ========================================")
printer.S(" inspect the pack itself")
blobs, _, err := repo.ListPack(ctx, id, int64(len(buf)))
if err != nil {
return fmt.Errorf("pack %v: %v", id.Str(), err)
}
checkPackSize(blobs, len(buf), printer)
if !blobsLoaded {
return loadBlobs(ctx, opts, repo, id, blobs, printer)
}
return nil
}
func checkPackSize(blobs restic.Blobs, fileSize int, printer progress.Printer) {
// track current size and offset
var size, offset uint64
blobs.Sort()
for _, pb := range blobs {
printer.S(" %v blob %v, offset %-6d, raw length %-6d", pb.Type, pb.ID, pb.Offset, pb.Length)
if offset != uint64(pb.Offset) {
printer.S(" hole in file, want offset %v, got %v", offset, pb.Offset)
}
offset = uint64(pb.Offset + pb.Length)
size += uint64(pb.Length)
}
size += uint64(pack.CalculateHeaderSize(blobs))
if uint64(fileSize) != size {
printer.S(" file sizes do not match: computed %v, file size is %v", size, fileSize)
} else {
printer.S(" file sizes match")
}
}
+5 -4
View File
@@ -12,6 +12,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@@ -83,7 +84,7 @@ type Comparer struct {
repo restic.BlobLoader
opts DiffOptions
printChange func(change *Change)
printError func(string, ...interface{})
printError func(string, ...any)
}
type Change struct {
@@ -157,7 +158,7 @@ type DiffStatsContainer struct {
}
// updateBlobs updates the blob counters in the stats struct.
func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *DiffStat, printError func(string, ...interface{})) {
func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *DiffStat, printError func(string, ...any)) {
for h := range blobs.Keys() {
switch h.Type {
case restic.DataBlob:
@@ -166,7 +167,7 @@ func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *Diff
stats.TreeBlobs++
}
size, found := repo.LookupBlobSize(h.Type, h.ID)
size, found := repo.LookupBlobSize(h)
if !found {
printError("unable to find blob size for %v", h)
continue
@@ -358,7 +359,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts global.Options, args [
return errors.Fatalf("specify two snapshot IDs")
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
+2 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -136,7 +137,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts global.Options, args [
return errors.Fatal("no file and no snapshot ID specified")
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
switch opts.Archive {
case "tar", "zip":
+54 -51
View File
@@ -19,6 +19,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/walker"
)
@@ -135,9 +136,9 @@ type statefulOutput struct {
oldsn *data.Snapshot
hits int
printer interface {
S(string, ...interface{})
P(string, ...interface{})
E(string, ...interface{})
S(string, ...any)
P(string, ...any)
E(string, ...any)
}
stdout io.Writer
}
@@ -213,7 +214,7 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *
Path string `json:"path"`
ParentTree string `json:"parent_tree,omitempty"`
SnapshotID string `json:"snapshot"`
Time time.Time `json:"time,omitempty"`
Time time.Time `json:"time"`
}{
ObjectType: kind,
ID: id,
@@ -280,9 +281,9 @@ type Finder struct {
treeIDs map[string]struct{}
itemsFound int
printer interface {
S(string, ...interface{})
P(string, ...interface{})
E(string, ...interface{})
S(string, ...any)
P(string, ...any)
E(string, ...any)
}
}
@@ -377,7 +378,7 @@ func (f *Finder) findTree(treeID restic.ID, nodepath string) error {
f.itemsFound++
// Terminate if we have found all trees (and we are not
// looking for blobs)
if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil {
if f.itemsFound >= len(f.treeIDs) && len(f.blobIDs) == 0 {
// Return an error to terminate the Walk
return errFindDone
}
@@ -412,13 +413,13 @@ func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
return nil
}
if node.Type == "dir" && f.treeIDs != nil {
if node.Type == "dir" && len(f.treeIDs) > 0 {
if err := f.findTree(*node.Subtree, nodepath); err != nil {
return err
}
}
if node.Type == data.NodeTypeFile && f.blobIDs != nil {
if node.Type == data.NodeTypeFile && len(f.blobIDs) > 0 {
for _, id := range node.Content {
if ctx.Err() != nil {
return ctx.Err()
@@ -444,6 +445,17 @@ func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
var errAllPacksFound = errors.New("all packs found")
func (f *Finder) addBlobHandle(h restic.BlobHandle) {
switch h.Type {
case restic.DataBlob:
f.blobIDs[h.ID.String()] = struct{}{}
case restic.TreeBlob:
f.treeIDs[h.ID.String()] = struct{}{}
default:
panic(fmt.Sprintf("unknown type %v in blob list", h.Type.String()))
}
}
// packsToBlobs converts the list of pack IDs to a list of blob IDs that
// belong to those packs.
func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
@@ -467,37 +479,30 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
return nil
}
delete(packIDs, id.Str())
} else {
// forget found id
delete(packIDs, idStr)
packIDs[idStr] = struct{}{}
}
debug.Log("Found pack %s", idStr)
blobs, _, err := f.repo.ListPack(ctx, id, size)
handles, err := f.repo.ListPackHandles(ctx, id, size)
if err != nil {
return err
// ignore error to allow fallback to index
return nil
}
for _, b := range blobs {
switch b.Type {
case restic.DataBlob:
f.blobIDs[b.ID.String()] = struct{}{}
case restic.TreeBlob:
f.treeIDs[b.ID.String()] = struct{}{}
default:
panic(fmt.Sprintf("unknown type %v in blob list", b.Type.String()))
}
for _, h := range handles {
f.addBlobHandle(h)
}
// forget successfully processed pack
delete(packIDs, idStr)
// Stop searching when all packs have been found
if len(packIDs) == 0 {
return errAllPacksFound
}
return nil
})
if err != nil && err != errAllPacksFound {
if err != nil && !errors.Is(err, errAllPacksFound) {
return err
}
if err != errAllPacksFound {
if len(packIDs) > 0 {
// try to resolve unknown pack ids from the index
packIDs, err = f.indexPacksToBlobs(ctx, packIDs)
if err != nil {
@@ -515,7 +520,7 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
return errors.Fatalf("unable to find pack(s): %v", list)
}
debug.Log("%d blobs found", len(f.blobIDs))
debug.Log("%d blobs %v trees found", len(f.blobIDs), len(f.treeIDs))
return nil
}
@@ -525,22 +530,23 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
// remember which packs were found in the index
indexPackIDs := make(map[string]struct{})
err := f.repo.ListBlobs(wctx, func(pb restic.PackedBlob) {
idStr := pb.PackID.String()
err := f.repo.ListBlobs(wctx, func(pb restic.PackBlob) {
packID := pb.PackID()
idStr := packID.String()
// keep entry in packIDs as Each() returns individual index entries
matchingID := false
if _, ok := packIDs[idStr]; ok {
matchingID = true
} else {
if _, ok := packIDs[pb.PackID.Str()]; ok {
if _, ok := packIDs[packID.Str()]; ok {
// expand id
delete(packIDs, pb.PackID.Str())
delete(packIDs, packID.Str())
packIDs[idStr] = struct{}{}
matchingID = true
}
}
if matchingID {
f.blobIDs[pb.ID.String()] = struct{}{}
f.addBlobHandle(pb.Handle())
indexPackIDs[idStr] = struct{}{}
}
})
@@ -552,13 +558,6 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
delete(packIDs, id)
}
if len(indexPackIDs) > 0 {
list := make([]string, 0, len(indexPackIDs))
for h := range indexPackIDs {
list = append(list, h)
}
f.printer.E("some pack files are missing from the repository, getting their blobs from the repository index: %v\n\n", list)
}
return packIDs, nil
}
@@ -569,16 +568,16 @@ func (f *Finder) findObjectPack(id string, t restic.BlobType) {
return
}
blobs := f.repo.LookupBlob(t, rid)
blobs := f.repo.LookupBlob(restic.BlobHandle{Type: t, ID: rid})
if len(blobs) == 0 {
f.printer.S("Object %s with type %s not found in the index", t.String(), rid.Str())
return
}
for _, b := range blobs {
if b.ID.Equal(rid) {
f.printer.S("Object belongs to pack %s", b.PackID)
f.printer.S(" ... Pack %s: %s", b.PackID.Str(), b.String())
if b.Handle().ID.Equal(rid) {
f.printer.S("Object belongs to pack %s", b.PackID())
f.printer.S(" ... Pack %s: %v", b.PackID().String(), b.Handle())
break
}
}
@@ -599,7 +598,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
return errors.Fatal("wrong number of arguments")
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
var err error
pat := findPattern{pattern: args}
@@ -676,11 +675,15 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
}
var filteredSnapshots []*data.Snapshot
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots, printer) {
err = opts.SnapshotFilter.FindAll(ctx, snapshotLister, repo, opts.Snapshots, func(_ string, sn *data.Snapshot, err error) error {
if err != nil {
return err
}
filteredSnapshots = append(filteredSnapshots, sn)
}
if ctx.Err() != nil {
return ctx.Err()
return nil
})
if err != nil {
return err
}
sort.Slice(filteredSnapshots, func(i, j int) bool {
@@ -691,7 +694,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
})
for _, sn := range filteredSnapshots {
if f.blobIDs != nil || f.treeIDs != nil {
if len(f.blobIDs) > 0 || len(f.treeIDs) > 0 {
if err = f.findIDs(ctx, sn); err != nil && !errors.Is(err, errFindDone) {
return err
}
@@ -703,7 +706,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
}
f.out.Finish()
if opts.ShowPackID && (f.blobIDs != nil || f.treeIDs != nil) {
if opts.ShowPackID && (len(f.blobIDs) > 0 || len(f.treeIDs) > 0) {
f.findObjectsPacks()
}
+37 -13
View File
@@ -9,10 +9,11 @@ import (
"testing"
"time"
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts global.Options, pattern string) []byte {
@@ -50,7 +51,7 @@ type testMatch struct {
Path string `json:"path,omitempty"`
Permissions string `json:"permissions,omitempty"`
Size uint64 `json:"size,omitempty"`
Date time.Time `json:"date,omitempty"`
Date time.Time `json:"date"`
UID uint32 `json:"uid,omitempty"`
GID uint32 `json:"gid,omitempty"`
}
@@ -153,7 +154,7 @@ type JSONOutput struct {
Path string `json:"path"`
ParentTree string `json:"parent_tree,omitempty"`
SnapshotID string `json:"snapshot"`
Time time.Time `json:"time,omitempty"`
Time time.Time `json:"time"`
}
func TestFindPackfile(t *testing.T) {
@@ -169,7 +170,7 @@ func TestFindPackfile(t *testing.T) {
// do all the testing wrapped inside withTermStatus()
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@@ -179,9 +180,10 @@ func TestFindPackfile(t *testing.T) {
packID := restic.ID{}
done := false
err = repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
if !done && pb.Type == restic.TreeBlob {
packID = pb.PackID
err = repo.ListBlobs(ctx, func(pb restic.PackBlob) {
h := pb.Handle()
if !done && h.Type == restic.TreeBlob {
packID = pb.PackID()
done = true
}
})
@@ -228,20 +230,20 @@ func TestFindPackID(t *testing.T) {
dataPackID := restic.ID{}
treePackID := restic.ID{}
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
// load Index
rtest.OK(t, repo.LoadIndex(ctx, nil))
rtest.OK(t, repo.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
// go through all index entries and collect data and tree packfile(s)
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
switch blob.Type {
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackBlob) {
switch blob.Handle().Type {
case restic.DataBlob:
dataPackID = blob.PackID
dataPackID = blob.PackID()
case restic.TreeBlob:
treePackID = blob.PackID
treePackID = blob.PackID()
}
}))
return nil
@@ -258,6 +260,28 @@ func TestFindPackID(t *testing.T) {
rtest.Assert(t, len(findRes) == numberOfFiles, "expected %d entries for this packfile, got %d",
numberOfFiles, len(findRes))
// corrupt the data pack file; find must fall back to the index
be := captureBackend(&env.gopts)
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
_, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
h := backend.Handle{Type: backend.PackFile, Name: dataPackID.String()}
rtest.OK(t, be().Remove(ctx, h))
buf := make([]byte, 10)
rtest.OK(t, be().Save(ctx, h, backend.NewByteReader(buf, be().Hasher())))
return nil
})
rtest.OK(t, err)
out = testRunFind(t, true, FindOptions{PackID: true}, env.gopts, dataPackID.String())
findRes = []JSONOutput{}
rtest.OK(t, json.Unmarshal(out, &findRes))
rtest.Assert(t, len(findRes) == numberOfFiles, "expected %d entries for broken packfile, got %d",
numberOfFiles, len(findRes))
// look for tree packfile
rtest.Assert(t, !treePackID.IsNull(), "expected to find tree packfile in repo")
packID = treePackID.String()
+11 -6
View File
@@ -12,6 +12,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@@ -189,7 +190,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for forget command")
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
if err != nil {
return err
@@ -199,11 +200,15 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
var snapshots data.Snapshots
removeSnIDs := restic.NewIDSet()
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
err = opts.SnapshotFilter.FindAll(ctx, repo, repo, args, func(_ string, sn *data.Snapshot, err error) error {
if err != nil {
return err
}
snapshots = append(snapshots, sn)
}
if ctx.Err() != nil {
return ctx.Err()
return nil
})
if err != nil {
return err
}
var jsonGroups []*ForgetGroup
@@ -348,7 +353,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs))
}
pruneOptions.DryRun = opts.DryRun
return runPruneWithRepo(ctx, pruneOptions, repo, removeSnIDs, printer)
return runPruneWithRepo(ctx, pruneOptions, gopts, repo, removeSnIDs, printer)
}
return nil
+4 -3
View File
@@ -7,6 +7,7 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
@@ -55,7 +56,7 @@ func (opts *generateOptions) AddFlags(f *pflag.FlagSet) {
f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
}
func writeManpages(root *cobra.Command, dir string, printer progress.Printer) error {
func writeManpages(root *cobra.Command, dir string, printer restic.Printer) error {
// use a fixed date for the man pages so that generating them is deterministic
date, err := time.Parse("Jan 2006", "Jan 2017")
if err != nil {
@@ -73,7 +74,7 @@ func writeManpages(root *cobra.Command, dir string, printer progress.Printer) er
return doc.GenManTree(root, header, dir)
}
func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts global.Options) (err error) {
func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer restic.Printer, gopts global.Options) (err error) {
printer.PT("writing %s completion file to %v", shell, filename)
var outWriter io.Writer
if filename != "-" {
@@ -116,7 +117,7 @@ func runGenerate(opts generateOptions, gopts global.Options, args []string, term
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
cmdRoot := newRootCommand(&global.Options{})
if opts.ManDir != "" {
+2 -2
View File
@@ -60,7 +60,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts global.Options, args [
return errors.Fatal("the init command expects no arguments, only options - please see `restic help init` for usage and flags")
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
var version uint
switch opts.RepositoryVersion {
@@ -108,7 +108,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts global.Options, args [
return nil
}
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer progress.Printer) (*chunker.Pol, error) {
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer restic.Printer) (*chunker.Pol, error) {
if opts.CopyChunkerParameters {
otherGopts, _, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "secondary")
if err != nil {
+4 -4
View File
@@ -10,13 +10,12 @@ import (
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/progress"
)
func testRunInit(t testing.TB, gopts global.Options) {
repository.TestUseLowSecurityKDFParameters(t)
restic.TestDisableCheckPolynomial(t)
restic.TestSetLockTimeout(t, 0)
repository.TestSetLockTimeout(t, 0)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runInit(ctx, InitOptions{}, gopts, nil, gopts.Term)
@@ -26,6 +25,7 @@ func testRunInit(t testing.TB, gopts global.Options) {
// create temporary junk files to verify that restic does not trip over them
for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
rtest.OK(t, os.MkdirAll(filepath.Join(gopts.Repo, path), 0700))
rtest.OK(t, os.WriteFile(filepath.Join(gopts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
}
}
@@ -57,14 +57,14 @@ func TestInitCopyChunkerParams(t *testing.T) {
var repo *repository.Repository
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
repo, err = global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
repo, err = global.OpenRepository(ctx, gopts, restic.NewNoopPrinter())
return err
})
rtest.OK(t, err)
var otherRepo *repository.Repository
err = withTermStatus(t, env2.gopts, func(ctx context.Context, gopts global.Options) error {
otherRepo, err = global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
otherRepo, err = global.OpenRepository(ctx, gopts, restic.NewNoopPrinter())
return err
})
rtest.OK(t, err)
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
@@ -60,7 +61,7 @@ func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, ar
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")
}
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false, printer)
if err != nil {
return err
@@ -70,7 +71,7 @@ func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, ar
return addKey(ctx, repo, gopts, opts, printer)
}
func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer progress.Printer) error {
func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer restic.Printer) error {
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
if err != nil {
return err
+9 -5
View File
@@ -3,6 +3,7 @@ package main
import (
"bufio"
"context"
"github.com/restic/restic/internal/restic"
"os"
"path/filepath"
"regexp"
@@ -13,7 +14,6 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/progress"
)
func testRunKeyListOtherIDs(t testing.TB, gopts global.Options) []string {
@@ -63,11 +63,13 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts global.Options) {
rtest.OK(t, err)
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
repo, err := global.OpenRepository(ctx, gopts, restic.NewNoopPrinter())
rtest.OK(t, err)
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 2, "")
err = repo.SearchKey(ctx, testKeyNewPassword, 2, "")
rtest.OK(t, err)
key, err := repository.LoadKey(ctx, repo, repo.KeyID())
rtest.OK(t, err)
rtest.Equals(t, "john", key.Username)
rtest.Equals(t, "example.com", key.Hostname)
return nil
@@ -105,11 +107,13 @@ func testRunKeyPasswdUserHost(t testing.TB, newPassword string, gopts global.Opt
gopts.Password = testKeyNewPassword
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
repo, err := global.OpenRepository(ctx, gopts, restic.NewNoopPrinter())
rtest.OK(t, err)
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 1, "")
err = repo.SearchKey(ctx, testKeyNewPassword, 1, "")
rtest.OK(t, err)
key, err := repository.LoadKey(ctx, repo, repo.KeyID())
rtest.OK(t, err)
rtest.Equals(t, "john", key.Username)
rtest.Equals(t, "example.com", key.Hostname)
return nil
+2 -2
View File
@@ -46,7 +46,7 @@ func runKeyList(ctx context.Context, gopts global.Options, args []string, term u
return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags")
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
return err
@@ -56,7 +56,7 @@ func runKeyList(ctx context.Context, gopts global.Options, args []string, term u
return listKeys(ctx, repo, gopts, printer)
}
func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer progress.Printer) error {
func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer restic.Printer) error {
type keyInfo struct {
Current bool `json:"current"`
ID string `json:"id"`
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
@@ -55,7 +56,7 @@ func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptio
return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags")
}
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
return err
@@ -65,7 +66,7 @@ func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptio
return changePassword(ctx, repo, gopts, opts, printer)
}
func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer progress.Printer) error {
func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer restic.Printer) error {
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
if err != nil {
return err
+2 -2
View File
@@ -43,7 +43,7 @@ func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term
return fmt.Errorf("key remove expects one argument as the key id")
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
return err
@@ -53,7 +53,7 @@ func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term
return deleteKey(ctx, repo, args[0], printer)
}
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer progress.Printer) error {
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer restic.Printer) error {
id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix)
if err != nil {
return err
+59 -19
View File
@@ -2,13 +2,16 @@ package main
import (
"context"
"fmt"
"strings"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
)
@@ -18,10 +21,12 @@ func newListCommand(globalOptions *global.Options) *cobra.Command {
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
cmd := &cobra.Command{
Use: "list [flags] [" + listAllowedArgsUseString + "]",
Use: "list [flags] [" + listAllowedArgsUseString + "|packs snapshotID]",
Short: "List objects in the repository",
Long: `
The "list" command allows listing objects in the repository based on type.
The "list packs snapshotID" variant accepts one snapshotID and lists all packfiles
used by this snapshot.
EXIT STATUS
===========
@@ -35,19 +40,18 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true,
GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), *globalOptions, args, globalOptions.Term)
return runList(cmd.Context(), *globalOptions, args, globalOptions.Term, listAllowedArgsUseString)
},
ValidArgs: listAllowedArgs,
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
}
return cmd
}
func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal, listAllowedArgsUseString string) error {
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
if len(args) != 1 {
return errors.Fatal("type not specified")
if len(args) == 0 || (args[0] == "packs" && len(args) > 2) || (args[0] != "packs" && len(args) != 1) {
return errors.Fatal(fmt.Sprintf("too many parameters or type not specified. Must be one of [%s] or 'packs snapshotID'", listAllowedArgsUseString))
}
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks", printer)
@@ -60,6 +64,10 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T
switch args[0] {
case "packs":
t = restic.PackFile
if len(args) == 2 {
// args[1] needs to be a snapshotID
return packfileList(ctx, repo, args[1], printer)
}
case "index":
t = restic.IndexFile
case "snapshots":
@@ -69,18 +77,13 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T
case "locks":
t = restic.LockFile
case "blobs":
return index.ForAllIndexes(ctx, repo, repo, func(_ restic.ID, idx *index.Index, err error) error {
if err != nil {
return err
for entry := range repository.AllIndexBlobs(ctx, repo, repo) {
if entry.Error != nil {
return entry.Error
}
for blobs := range idx.Values() {
if ctx.Err() != nil {
return ctx.Err()
}
printer.S("%v %v", blobs.Type, blobs.ID)
}
return nil
})
printer.S("%v %v", entry.Handle.Type, entry.Handle.ID)
}
return nil
default:
return errors.Fatal("invalid type")
}
@@ -90,3 +93,40 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T
return nil
})
}
// packfileList handles the list packs <snapshotID> variant.
// It prints a sorted list of packfiles belonging to this snapshot.
func packfileList(ctx context.Context, repo restic.Repository, snapshotID string, printer restic.Printer) error {
// ignore subpaths as this command is intended to list all packfiles necessary to restore the snapshot
// subpaths would require special handling and limit restorability
sn, _, err := (&data.SnapshotFilter{}).FindLatest(ctx, repo, repo, snapshotID)
if err != nil {
return fmt.Errorf("failed to find snapshot: %v", err)
}
if err = repo.LoadIndex(ctx, printer); err != nil {
return err
}
usedBlobs := repo.NewAssociatedBlobSet()
bar := printer.NewCounter("snapshot")
bar.SetMax(uint64(1))
err = data.FindUsedBlobs(ctx, repo, []restic.ID{*sn.Tree}, usedBlobs, bar)
bar.Done()
if err != nil {
return err
}
snapPacks := restic.NewIDSet()
for bh := range usedBlobs.Keys() {
for _, blob := range repo.LookupBlob(bh) {
snapPacks.Insert(blob.PackID())
}
}
for _, packID := range snapPacks.List() {
printer.S("%v", packID)
}
return nil
}
+28 -7
View File
@@ -11,12 +11,12 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
func testRunList(t testing.TB, gopts global.Options, tpe string) restic.IDs {
func testRunList(t testing.TB, gopts global.Options, params ...string) restic.IDs {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runList(ctx, gopts, []string{tpe}, gopts.Term)
return runList(ctx, gopts, params, gopts.Term, "")
})
rtest.OK(t, err)
return parseIDsFromReader(t, buf)
@@ -61,18 +61,18 @@ func testListSnapshots(t testing.TB, gopts global.Options, expected int) restic.
// extract blob set from repository index
func testListBlobs(t testing.TB, gopts global.Options) (blobSetFromIndex restic.IDSet) {
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
// make sure the index is loaded
rtest.OK(t, repo.LoadIndex(ctx, nil))
rtest.OK(t, repo.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
// get blobs from index
blobSetFromIndex = restic.NewIDSet()
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
blobSetFromIndex.Insert(blob.ID)
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackBlob) {
blobSetFromIndex.Insert(blob.Handle().ID)
}))
return nil
})
@@ -102,3 +102,24 @@ func TestListBlobs(t *testing.T) {
rtest.Assert(t, blobSetFromIndex.Equals(testIDSet), "the set of restic.ID s should be equal")
}
func TestPackfileListWithSnapshot(t *testing.T) {
// setup
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
// 3 backups, single file each
opts := BackupOptions{}
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "40")}, opts, env.gopts)
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "41")}, opts, env.gopts)
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "42")}, opts, env.gopts)
testListSnapshots(t, env.gopts, 3)
// run packfilelist
packfiles := testRunList(t, env.gopts, "packs")
rtest.Assert(t, len(packfiles) == 6, "expected 6 packfiles in repository, got %d", len(packfiles))
packfiles = testRunList(t, env.gopts, "packs", "latest")
rtest.Assert(t, len(packfiles) == 2, "expected 2 packfiles in snapshot, got %d", len(packfiles))
}
+78 -69
View File
@@ -21,6 +21,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/walker"
)
@@ -93,7 +94,7 @@ func (opts *LsOptions) AddFlags(f *pflag.FlagSet) {
type lsPrinter interface {
Snapshot(sn *data.Snapshot) error
Node(path string, node *data.Node, isPrefixDirectory bool) error
NodeOutput(n lsNodeOutput, isPrefixDirectory bool) error
LeaveDir(path string) error
Close() error
}
@@ -120,55 +121,68 @@ func (p *jsonLsPrinter) Snapshot(sn *data.Snapshot) error {
})
}
// Node formats node in our custom JSON format, followed by a newline.
func (p *jsonLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
if isPrefixDirectory {
return nil
}
return lsNodeJSON(p.enc, path, node)
type lsNodeOutput struct {
Name string `json:"name"`
Type data.NodeType `json:"type"`
Path string `json:"path"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
Size *uint64 `json:"size,omitempty"`
Mode os.FileMode `json:"mode,omitempty"`
Permissions string `json:"permissions,omitempty"`
ModTime time.Time `json:"mtime"`
AccessTime time.Time `json:"atime"`
ChangeTime time.Time `json:"ctime"`
Inode uint64 `json:"inode,omitempty"`
MessageType string `json:"message_type"`
StructType string `json:"struct_type"`
// Used for the ncdu output only
LinkTarget string `json:"-"`
DeviceID uint64 `json:"-"`
Links uint64 `json:"-"`
}
func lsNodeJSON(enc *json.Encoder, path string, node *data.Node) error {
n := &struct {
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
Size *uint64 `json:"size,omitempty"`
Mode os.FileMode `json:"mode,omitempty"`
Permissions string `json:"permissions,omitempty"`
ModTime time.Time `json:"mtime,omitempty"`
AccessTime time.Time `json:"atime,omitempty"`
ChangeTime time.Time `json:"ctime,omitempty"`
Inode uint64 `json:"inode,omitempty"`
MessageType string `json:"message_type"` // "node"
StructType string `json:"struct_type"` // "node", deprecated
func (n lsNodeOutput) fileSize() uint64 {
if n.Size == nil {
return 0
}
return *n.Size
}
size uint64 // Target for Size pointer.
}{
func lsNodeOutputFrom(path string, node *data.Node) lsNodeOutput {
n := lsNodeOutput{
Name: node.Name,
Type: string(node.Type),
Type: node.Type,
Path: path,
UID: node.UID,
GID: node.GID,
size: node.Size,
Mode: node.Mode,
Permissions: node.Mode.String(),
ModTime: node.ModTime,
AccessTime: node.AccessTime,
ChangeTime: node.ChangeTime,
Inode: node.Inode,
LinkTarget: node.LinkTarget,
DeviceID: node.DeviceID,
Links: node.Links,
MessageType: "node",
StructType: "node",
}
// Always print size for regular files, even when empty,
// but never for other types.
if node.Type == data.NodeTypeFile {
n.Size = &n.size
size := node.Size
n.Size = &size
}
return n
}
return enc.Encode(n)
func (p *jsonLsPrinter) NodeOutput(n lsNodeOutput, isPrefixDirectory bool) error {
if isPrefixDirectory {
return nil
}
return p.enc.Encode(n)
}
func (p *jsonLsPrinter) LeaveDir(_ string) error { return nil }
@@ -195,7 +209,7 @@ func (p *ncduLsPrinter) Snapshot(sn *data.Snapshot) error {
return err
}
func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
func lsNcduOutput(node lsNodeOutput) ([]byte, error) {
type NcduNode struct {
Name string `json:"name"`
Asize uint64 `json:"asize"`
@@ -212,11 +226,12 @@ func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
const blockSize = 512
size := node.fileSize()
outNode := NcduNode{
Name: node.Name,
Asize: node.Size,
Asize: size,
// round up to nearest full blocksize
Dsize: (node.Size + blockSize - 1) / blockSize * blockSize,
Dsize: (size + blockSize - 1) / blockSize * blockSize,
Dev: node.DeviceID,
Ino: node.Inode,
NLink: node.Links,
@@ -244,8 +259,8 @@ func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
return json.Marshal(outNode)
}
func (p *ncduLsPrinter) Node(path string, node *data.Node, _ bool) error {
out, err := lsNcduNode(path, node)
func (p *ncduLsPrinter) NodeOutput(node lsNodeOutput, _ bool) error {
out, err := lsNcduOutput(node)
if err != nil {
return err
}
@@ -275,8 +290,8 @@ type textLsPrinter struct {
ListLong bool
HumanReadable bool
termPrinter interface {
P(msg string, args ...interface{})
S(msg string, args ...interface{})
P(msg string, args ...any)
S(msg string, args ...any)
}
}
@@ -284,9 +299,9 @@ func (p *textLsPrinter) Snapshot(sn *data.Snapshot) error {
p.termPrinter.P("%v filtered by %v:", sn, p.dirs)
return nil
}
func (p *textLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
func (p *textLsPrinter) NodeOutput(node lsNodeOutput, isPrefixDirectory bool) error {
if !isPrefixDirectory {
p.termPrinter.S("%s", formatNode(path, node, p.ListLong, p.HumanReadable))
p.termPrinter.S("%s", formatNodeOutput(node, p.ListLong, p.HumanReadable))
}
return nil
}
@@ -298,14 +313,8 @@ func (p *textLsPrinter) Close() error {
return nil
}
// for ls -l output sorting
type toSortOutput struct {
nodepath string
node *data.Node
}
func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []string, term ui.Terminal) error {
termPrinter := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
termPrinter := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
if len(args) == 0 {
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
@@ -431,7 +440,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
printedDir := false
if withinDir(nodepath) {
// if we're within a target path, print the node
if err := printer.Node(nodepath, node, false); err != nil {
if err := printer.NodeOutput(lsNodeOutputFrom(nodepath, node), false); err != nil {
return err
}
printedDir = true
@@ -448,7 +457,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
if approachingMatchingTree(nodepath) {
// print node leading up to the target paths
if !printedDir {
return printer.Node(nodepath, node, true)
return printer.NodeOutput(lsNodeOutputFrom(nodepath, node), true)
}
return nil
}
@@ -487,7 +496,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
type sortedPrinter struct {
printer lsPrinter
collector []toSortOutput
collector []lsNodeOutput
sortMode SortMode
reverse bool
}
@@ -495,9 +504,9 @@ type sortedPrinter struct {
func (p *sortedPrinter) Snapshot(sn *data.Snapshot) error {
return p.printer.Snapshot(sn)
}
func (p *sortedPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
func (p *sortedPrinter) NodeOutput(node lsNodeOutput, isPrefixDirectory bool) error {
if !isPrefixDirectory {
p.collector = append(p.collector, toSortOutput{path, node})
p.collector = append(p.collector, node)
}
return nil
}
@@ -506,49 +515,49 @@ func (p *sortedPrinter) LeaveDir(_ string) error {
return nil
}
func (p *sortedPrinter) Close() error {
var comparator func(a, b toSortOutput) int
var comparator func(a, b lsNodeOutput) int
switch p.sortMode {
case SortModeName:
case SortModeSize:
comparator = func(a, b toSortOutput) int {
comparator = func(a, b lsNodeOutput) int {
return cmp.Or(
cmp.Compare(a.node.Size, b.node.Size),
cmp.Compare(a.nodepath, b.nodepath),
cmp.Compare(a.fileSize(), b.fileSize()),
cmp.Compare(a.Path, b.Path),
)
}
case SortModeMtime:
comparator = func(a, b toSortOutput) int {
comparator = func(a, b lsNodeOutput) int {
return cmp.Or(
a.node.ModTime.Compare(b.node.ModTime),
cmp.Compare(a.nodepath, b.nodepath),
a.ModTime.Compare(b.ModTime),
cmp.Compare(a.Path, b.Path),
)
}
case SortModeAtime:
comparator = func(a, b toSortOutput) int {
comparator = func(a, b lsNodeOutput) int {
return cmp.Or(
a.node.AccessTime.Compare(b.node.AccessTime),
cmp.Compare(a.nodepath, b.nodepath),
a.AccessTime.Compare(b.AccessTime),
cmp.Compare(a.Path, b.Path),
)
}
case SortModeCtime:
comparator = func(a, b toSortOutput) int {
comparator = func(a, b lsNodeOutput) int {
return cmp.Or(
a.node.ChangeTime.Compare(b.node.ChangeTime),
cmp.Compare(a.nodepath, b.nodepath),
a.ChangeTime.Compare(b.ChangeTime),
cmp.Compare(a.Path, b.Path),
)
}
case SortModeExt:
// map name to extension
mapExt := make(map[string]string, len(p.collector))
for _, item := range p.collector {
ext := filepath.Ext(item.nodepath)
mapExt[item.nodepath] = ext
ext := filepath.Ext(item.Path)
mapExt[item.Path] = ext
}
comparator = func(a, b toSortOutput) int {
comparator = func(a, b lsNodeOutput) int {
return cmp.Or(
cmp.Compare(mapExt[a.nodepath], mapExt[b.nodepath]),
cmp.Compare(a.nodepath, b.nodepath),
cmp.Compare(mapExt[a.Path], mapExt[b.Path]),
cmp.Compare(a.Path, b.Path),
)
}
}
@@ -560,7 +569,7 @@ func (p *sortedPrinter) Close() error {
slices.Reverse(p.collector)
}
for _, elem := range p.collector {
if err := p.printer.Node(elem.nodepath, elem.node, false); err != nil {
if err := p.printer.NodeOutput(elem, false); err != nil {
return err
}
}
+10 -10
View File
@@ -96,12 +96,12 @@ func TestLsNodeJSON(t *testing.T) {
c := lsTestNodes[i]
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
err := lsNodeJSON(enc, c.path, &c.Node)
err := enc.Encode(lsNodeOutputFrom(c.path, &c.Node))
rtest.OK(t, err)
rtest.Equals(t, expect+"\n", buf.String())
// Sanity check: output must be valid JSON.
var v interface{}
var v any
err = json.NewDecoder(buf).Decode(&v)
rtest.OK(t, err)
}
@@ -116,12 +116,12 @@ func TestLsNcduNode(t *testing.T) {
`{"name":"sticky","asize":0,"dsize":0,"dev":0,"ino":0,"nlink":0,"notreg":false,"uid":0,"gid":0,"mode":4077,"mtime":0}`,
} {
c := lsTestNodes[i]
out, err := lsNcduNode(c.path, &c.Node)
out, err := lsNcduOutput(lsNodeOutputFrom(c.path, &c.Node))
rtest.OK(t, err)
rtest.Equals(t, expect, string(out))
// Sanity check: output must be valid JSON.
var v interface{}
var v any
err = json.Unmarshal(out, &v)
rtest.OK(t, err)
}
@@ -138,24 +138,24 @@ func TestLsNcdu(t *testing.T) {
Hostname: "host",
Paths: []string{"/example"},
}))
rtest.OK(t, printer.Node("/directory", &data.Node{
rtest.OK(t, printer.NodeOutput(lsNodeOutputFrom("/directory", &data.Node{
Type: data.NodeTypeDir,
Name: "directory",
ModTime: modTime,
}, false))
rtest.OK(t, printer.Node("/directory/data", &data.Node{
}), false))
rtest.OK(t, printer.NodeOutput(lsNodeOutputFrom("/directory/data", &data.Node{
Type: data.NodeTypeFile,
Name: "data",
Size: 42,
ModTime: modTime,
}, false))
}), false))
rtest.OK(t, printer.LeaveDir("/directory"))
rtest.OK(t, printer.Node("/file", &data.Node{
rtest.OK(t, printer.NodeOutput(lsNodeOutputFrom("/file", &data.Node{
Type: data.NodeTypeFile,
Name: "file",
Size: 12345,
ModTime: modTime,
}, false))
}), false))
rtest.OK(t, printer.Close())
rtest.Equals(t, `[1, 2, {"time":"0001-01-01T00:00:00Z","tree":null,"paths":["/example"],"hostname":"host"}, [{"name":"/"},
+3 -3
View File
@@ -53,7 +53,7 @@ func (opts *MigrateOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVarP(&opts.Force, "force", "f", false, `apply a migration a second time`)
}
func checkMigrations(ctx context.Context, repo restic.Repository, printer progress.Printer) error {
func checkMigrations(ctx context.Context, repo restic.Repository, printer restic.Printer) error {
printer.P("available migrations:\n")
found := false
@@ -76,7 +76,7 @@ func checkMigrations(ctx context.Context, repo restic.Repository, printer progre
return nil
}
func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Options, repo restic.Repository, args []string, term ui.Terminal, printer progress.Printer) error {
func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Options, repo restic.Repository, args []string, term ui.Terminal, printer restic.Printer) error {
var firsterr error
for _, name := range args {
found := false
@@ -135,7 +135,7 @@ func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Opti
}
func runMigrate(ctx context.Context, opts MigrateOptions, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
+8 -20
View File
@@ -19,13 +19,15 @@ import (
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/fuse"
systemFuse "github.com/anacrolix/fuse"
"github.com/anacrolix/fuse/fs"
fusefs "github.com/anacrolix/fuse/fs"
)
func registerMountCommand(cmdRoot *cobra.Command, globalOptions *global.Options) {
@@ -119,7 +121,7 @@ func (opts *MountOptions) AddFlags(f *pflag.FlagSet) {
}
func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
if opts.TimeTemplate == "" {
return errors.Fatal("time template string cannot be empty")
@@ -169,7 +171,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args
}
}
systemFuse.Debug = func(msg interface{}) {
systemFuse.Debug = func(msg any) {
debug.Log("fuse: %v", msg)
}
@@ -196,7 +198,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args
go func() {
defer close(done)
err = fs.Serve(c, root)
err = fusefs.Serve(c, root)
}()
printer.S("Now serving the repository at %s", mountpoint)
@@ -270,9 +272,9 @@ func checkMountpointOverlap(repoPath, mountpoint string) error {
switch {
case rp == mp:
return errors.Fatal(fmt.Sprintf("mountpoint %s is the local repository directory%s", mp, tail))
case isInside(rp, mp):
case fs.HasPathPrefix(rp, mp):
return errors.Fatal(fmt.Sprintf("mountpoint %s is inside the local repository directory %s%s", mp, rp, tail))
case isInside(mp, rp):
case fs.HasPathPrefix(mp, rp):
return errors.Fatal(fmt.Sprintf("local repository directory %s is inside the mountpoint %s%s", rp, mp, tail))
}
return nil
@@ -293,17 +295,3 @@ func resolvePath(p string) (string, error) {
}
return resolved, nil
}
// isInside reports whether child is strictly nested inside parent. Both paths
// must already be cleaned and absolute. Equal paths return false; the caller
// handles equality separately so it can produce a distinct error message.
func isInside(parent, child string) bool {
rel, err := filepath.Rel(parent, child)
if err != nil {
return false
}
if rel == "." || rel == ".." {
return false
}
return !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
+6 -12
View File
@@ -14,16 +14,15 @@ import (
systemFuse "github.com/anacrolix/fuse"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
const (
mountWait = 20
mountSleep = 100 * time.Millisecond
mountWait = 400
mountSleep = 5 * time.Millisecond
mountTestSubdir = "snapshots"
)
@@ -47,7 +46,7 @@ func snapshotsDirExists(t testing.TB, dir string) bool {
// waitForMount blocks (max mountWait * mountSleep) until the subdir
// "snapshots" appears in the dir.
func waitForMount(t testing.TB, dir string) {
for i := 0; i < mountWait; i++ {
for range mountWait {
if snapshotsDirExists(t, dir) {
t.Log("mounted directory is ready")
return
@@ -71,7 +70,7 @@ func testRunMount(t testing.TB, gopts global.Options, dir string, wg *sync.WaitG
func testRunUmount(t testing.TB, dir string) {
var err error
for i := 0; i < mountWait; i++ {
for range mountWait {
if err = systemFuse.Unmount(dir); err == nil {
t.Logf("directory %v umounted", dir)
return
@@ -131,7 +130,7 @@ func checkSnapshots(t testing.TB, gopts global.Options, mountpoint string, snaps
}
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
if err != nil {
return err
@@ -274,11 +273,6 @@ func TestMountSameTimestamps(t *testing.T) {
t.Skip("Skipping fuse tests")
}
debugEnabled := debug.TestLogToStderr(t)
if debugEnabled {
defer debug.TestDisableLog(t)
}
env, cleanup := withTestEnvironment(t)
// must list snapshots more than once
env.gopts.BackendTestHook = nil
+18 -19
View File
@@ -175,7 +175,7 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts global.Options, term
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for prune command")
}
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
if err != nil {
return err
@@ -190,11 +190,11 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts global.Options, term
opts.unsafeRecovery = true
}
return runPruneWithRepo(ctx, opts, repo, restic.NewIDSet(), printer)
return runPruneWithRepo(ctx, opts, gopts, repo, restic.NewIDSet(), printer)
}
func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer progress.Printer) error {
if repo.Cache() == nil {
func runPruneWithRepo(ctx context.Context, opts PruneOptions, gopts global.Options, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer restic.Printer) error {
if repo.Cache() == nil && !gopts.JSON {
printer.S("warning: running prune without a cache, this may be very slow!")
}
@@ -230,9 +230,13 @@ func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.R
printer.P("\nWould have made the following changes:")
}
err = printPruneStats(printer, plan.Stats())
if err != nil {
return err
if !gopts.JSON {
err = printPruneStats(printer, plan.Stats())
if err != nil {
return err
}
} else {
gopts.Term.Print(ui.ToJSONString(plan.Stats()))
}
// Trigger GC to reset garbage collection threshold
@@ -242,7 +246,7 @@ func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.R
}
// printPruneStats prints out the statistics
func printPruneStats(printer progress.Printer, stats repository.PruneStats) error {
func printPruneStats(printer restic.Printer, stats repository.PruneStats) error {
printer.V("\nused: %10d blobs / %s", stats.Blobs.Used, ui.FormatBytes(stats.Size.Used))
if stats.Blobs.Duplicate > 0 {
printer.V("duplicates: %10d blobs / %s", stats.Blobs.Duplicate, ui.FormatBytes(stats.Size.Duplicate))
@@ -251,24 +255,19 @@ func printPruneStats(printer progress.Printer, stats repository.PruneStats) erro
if stats.Size.Unref > 0 {
printer.V("unreferenced: %s", 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
printer.V("total: %10d blobs / %s", totalBlobs, ui.FormatBytes(totalSize))
printer.V("unused size: %s of total size", ui.FormatPercent(unusedSize, totalSize))
printer.V("total: %10d blobs / %s", stats.Blobs.Total, ui.FormatBytes(stats.Size.Total))
printer.V("unused size: %s of total size", ui.FormatPercent(stats.Size.Duplicate+stats.Size.Unused, stats.Size.Total))
printer.P("\nto repack: %10d blobs / %s", stats.Blobs.Repack, ui.FormatBytes(stats.Size.Repack))
printer.P("this removes: %10d blobs / %s", stats.Blobs.Repackrm, ui.FormatBytes(stats.Size.Repackrm))
printer.P("to delete: %10d blobs / %s", stats.Blobs.Remove, ui.FormatBytes(stats.Size.Remove+stats.Size.Unref))
totalPruneSize := stats.Size.Remove + stats.Size.Repackrm + stats.Size.Unref
printer.P("total prune: %10d blobs / %s", stats.Blobs.Remove+stats.Blobs.Repackrm, ui.FormatBytes(totalPruneSize))
printer.P("total prune: %10d blobs / %s", stats.Blobs.RemoveTotal, ui.FormatBytes(stats.Size.RemoveTotal))
if stats.Size.Uncompressed > 0 {
printer.P("not yet compressed: %s", ui.FormatBytes(stats.Size.Uncompressed))
}
printer.P("remaining: %10d blobs / %s", totalBlobs-(stats.Blobs.Remove+stats.Blobs.Repackrm), ui.FormatBytes(totalSize-totalPruneSize))
unusedAfter := unusedSize - stats.Size.Remove - stats.Size.Repackrm
printer.P("remaining: %10d blobs / %s", stats.Blobs.Remain, ui.FormatBytes(stats.Size.Remain))
printer.P("unused size after prune: %s (%s of remaining size)",
ui.FormatBytes(unusedAfter), ui.FormatPercent(unusedAfter, totalSize-totalPruneSize))
ui.FormatBytes(stats.Size.RemainUnused), ui.FormatPercent(stats.Size.RemainUnused, stats.Size.Remain))
printer.P("")
printer.V("totally used packs: %10d", stats.Packs.Used)
printer.V("partly used packs: %10d", stats.Packs.PartlyUsed)
@@ -283,7 +282,7 @@ func printPruneStats(printer progress.Printer, stats repository.PruneStats) erro
return nil
}
func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet, ignoreSnapshots restic.IDSet, printer progress.Printer) error {
func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet, ignoreSnapshots restic.IDSet, printer restic.Printer) error {
var snapshotTrees restic.IDs
printer.P("loading all snapshots...")
err := data.ForAllSnapshots(ctx, repo, repo, ignoreSnapshots,
+25
View File
@@ -258,3 +258,28 @@ func TestPruneRepackSmallerThanSmoke(t *testing.T) {
MaxUnused: "5%",
})
}
func TestPruneJSON(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
createPrunableRepo(t, env)
buf, err := withCaptureStdout(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
gopts.JSON = true
oldHook := gopts.BackendTestHook
gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) { return newListOnceBackend(r), nil }
defer func() {
gopts.BackendTestHook = oldHook
}()
return runPrune(ctx, pruneDefaultOptions, gopts, gopts.Term)
})
rtest.OK(t, err)
var stats repository.PruneStats
rtest.OK(t, json.Unmarshal(buf.Bytes(), &stats))
rtest.Equals(t, "summary", stats.MessageType)
rtest.Assert(t, stats.Blobs.Total > 0, "expected non-zero total blobs, got %v", stats.Blobs.Total)
rtest.Assert(t, stats.Packs.Total > 0, "expected non-zero total packs, got %v", stats.Packs.Total)
}
+6 -5
View File
@@ -48,7 +48,7 @@ func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) err
return err
}
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
return err
@@ -75,9 +75,10 @@ func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) err
// tree. If it is not referenced, we have a root tree.
trees := make(map[restic.ID]bool)
err = repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
if blob.Type == restic.TreeBlob {
trees[blob.Blob.ID] = false
err = repo.ListBlobs(ctx, func(blob restic.PackBlob) {
h := blob.Handle()
if h.Type == restic.TreeBlob {
trees[h.ID] = false
}
})
if err != nil {
@@ -173,7 +174,7 @@ func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) err
}
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, printer restic.Printer, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error {
sn, err := data.NewSnapshot([]string{name}, tags, hostname, time.Now())
if err != nil {
return errors.Fatalf("unable to save snapshot: %v", err)
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@@ -70,7 +71,7 @@ func newRebuildIndexCommand(globalOptions *global.Options) *cobra.Command {
}
func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts global.Options, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
@@ -11,7 +11,7 @@ import (
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
@@ -24,13 +24,14 @@ func testRunRebuildIndex(t testing.TB, gopts global.Options) {
}
func testRebuildIndex(t *testing.T, backendTestHook global.BackendWrapper) {
repository.TestInjectKey(t, restic.TestParseID("b9883c60bed42db51be171ca52f055104b6ea7cfa2bc381c05b2b1f78231280c"), `{"mac":{"k":"maQ4ILA872XnDxHVEno94A==","r":"OptMBABwkgIsMQcHME8cBw=="},"encrypt":"janrR1efN7HyQ8kOZ9zhHixooZ/e+WelH0mT4v9WskQ="}`)
env, cleanup := withTestEnvironment(t)
defer cleanup()
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
rtest.SetupTarTestFixture(t, env.base, datafile)
out, err := testRunCheckOutput(t, env.gopts, false)
out, _, err := testRunCheckOutput(t, env.gopts, false)
if !strings.Contains(out, "contained in several indexes") {
t.Fatalf("did not find checker hint for packs in several indexes")
}
@@ -47,7 +48,7 @@ func testRebuildIndex(t *testing.T, backendTestHook global.BackendWrapper) {
testRunRebuildIndex(t, env.gopts)
env.gopts.BackendTestHook = nil
out, err = testRunCheckOutput(t, env.gopts, false)
out, _, err = testRunCheckOutput(t, env.gopts, false)
if len(out) != 0 {
t.Fatalf("expected no output from the checker, got: %v", out)
}
@@ -61,15 +62,6 @@ func TestRebuildIndex(t *testing.T) {
testRebuildIndex(t, nil)
}
func TestRebuildIndexAlwaysFull(t *testing.T) {
indexFull := index.Full
defer func() {
index.Full = indexFull
}()
index.Full = func(*index.Index) bool { return true }
testRebuildIndex(t, nil)
}
// indexErrorBackend modifies the first index after reading.
type indexErrorBackend struct {
backend.Backend
@@ -82,7 +74,7 @@ func (b *indexErrorBackend) Load(ctx context.Context, h backend.Handle, length i
// protect hasErred
b.lock.Lock()
defer b.lock.Unlock()
if !b.hasErred && h.Type == restic.IndexFile {
if !b.hasErred && h.Type == backend.IndexFile {
b.hasErred = true
return consumer(errorReadCloser{rd})
}
@@ -120,6 +112,7 @@ func (b *appendOnlyBackend) Remove(_ context.Context, h backend.Handle) error {
}
func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
repository.TestInjectKey(t, restic.TestParseID("b9883c60bed42db51be171ca52f055104b6ea7cfa2bc381c05b2b1f78231280c"), `{"mac":{"k":"maQ4ILA872XnDxHVEno94A==","r":"OptMBABwkgIsMQcHME8cBw=="},"encrypt":"janrR1efN7HyQ8kOZ9zhHixooZ/e+WelH0mT4v9WskQ="}`)
env, cleanup := withTestEnvironment(t)
defer cleanup()
+9 -4
View File
@@ -11,6 +11,7 @@ import (
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
)
@@ -52,7 +53,7 @@ func runRepairPacks(ctx context.Context, gopts global.Options, term ui.Terminal,
return errors.Fatal("no ids specified")
}
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
@@ -68,9 +69,13 @@ func runRepairPacks(ctx context.Context, gopts global.Options, term ui.Terminal,
printer.P("saving backup copies of pack files to current folder")
for id := range ids {
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
// corrupted data is fine
if buf == nil {
return err
if ctx.Err() != nil {
return ctx.Err()
}
// only skip creating a local copy if no data at all could be loaded
if err != nil && buf == nil {
printer.E("will remove packfile %v due to failed download: %v", id, err)
continue
}
f, err := os.OpenFile("pack-"+id.String(), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
@@ -0,0 +1,75 @@
package main
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/progress"
)
// testRunRepairPacks runs `restic repair packs` with capturing stdout and stderr
func testRunRepairPacks(t testing.TB, gopts global.Options, args []string) (string, string, error) {
bufStdout, bufStderr, err := withCaptureStdoutStderr(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runRepairPacks(ctx, gopts, gopts.Term, args)
})
return bufStdout.String(), bufStderr.String(), err
}
func TestRunRepairPackfiles(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
// backup of subtree 0/0/9/42
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "42")}, BackupOptions{}, env.gopts)
testListSnapshots(t, env.gopts, 1)
packfileID := restic.ID{}
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
rtest.OK(t, repo.LoadIndex(ctx, printer))
// load packfiles from master index
err = repo.ListBlobs(ctx, func(blob restic.PackBlob) {
if blob.Handle().Type == restic.DataBlob {
packfileID = blob.PackID()
return
}
})
rtest.OK(t, err)
return nil
})
rtest.OK(t, err)
rtest.Assert(t, !packfileID.IsNull(), "expected valid packfile ID")
packIDString := packfileID.String()
filename := filepath.Join(env.gopts.Repo, "data", packIDString[0:2], packIDString)
rtest.OK(t, os.Remove(filename))
_, outError, err := testRunCheckOutput(t, env.gopts, false)
rtest.Assert(t, err != nil, "expected check errors, got none")
rtest.Assert(t, strings.Contains(string(outError), packIDString), "expected mention of %q", packIDString)
// change to temporary directory to not pollute the repository with backup files
cleanupChdir := rtest.Chdir(t, env.base)
defer cleanupChdir()
// restic repair packs 'packIDString'
_, _, err = testRunRepairPacks(t, env.gopts, []string{packIDString})
rtest.OK(t, err)
// run restic repair snapshots --forget
testRunRepairSnapshot(t, env.gopts, true)
_, _, err = testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
+50 -7
View File
@@ -9,6 +9,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/walker"
"github.com/spf13/cobra"
@@ -77,8 +78,39 @@ func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) {
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
}
// handleUnreadableSnapshotFile is called when FindAll returns an error for ID 'id'
func handleUnreadableSnapshotFile(
ctx context.Context,
be restic.Lister,
repo restic.Repository,
opts RepairOptions,
id string,
args []string,
printer restic.Printer,
) (bool, error) {
brokenID, err := restic.Find(ctx, be, restic.SnapshotFile, id)
if err != nil {
return false, err
}
if opts.Forget && slices.Index(args, id) >= 0 {
if opts.DryRun {
printer.P("would remove unreadable snapshot %v", brokenID)
return true, nil
}
if err := repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, brokenID); err != nil {
return false, errors.Wrapf(err, "unable to remove unreadable snapshot file %v", brokenID)
}
printer.P("removed unreadable snapshot %v", brokenID)
return true, nil
}
return false, errors.Fatalf("snapshot file %[1]s is unreadable, use `restic repair snapshots --forget %[1]s` to remove it. Original error: %v", brokenID, err)
}
func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOptions, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun, printer)
if err != nil {
@@ -95,10 +127,11 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
return err
}
// Three error cases are checked:
// Four error cases are checked:
// - tree is a nil tree (-> will be replaced by an empty tree)
// - trees which cannot be loaded (-> the tree contents will be removed)
// - files whose contents are not fully available (-> file will be modified)
// - *checker.SnapshotError
rewriter := walker.NewTreeRewriter(walker.RewriteOpts{
RewriteNode: func(node *data.Node, path string) *data.Node {
if node.Type == data.NodeTypeIrregular || node.Type == data.NodeTypeInvalid {
@@ -114,7 +147,7 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
var newSize uint64
// check all contents and remove if not available
for _, id := range node.Content {
if size, found := repo.LookupBlobSize(restic.DataBlob, id); !found {
if size, found := repo.LookupBlobSize(restic.BlobHandle{Type: restic.DataBlob, ID: id}); !found {
ok = false
} else {
newContent = append(newContent, id)
@@ -145,7 +178,15 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
})
changedCount := 0
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) {
errOuter := opts.SnapshotFilter.FindAll(ctx, snapshotLister, repo, args, func(id string, sn *data.Snapshot, err error) error {
if err != nil {
changed, err := handleUnreadableSnapshotFile(ctx, snapshotLister, repo, opts, id, args, printer)
if changed {
changedCount++
}
return err
}
printer.P("\n%v", sn)
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error) {
@@ -158,9 +199,11 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
if changed {
changedCount++
}
}
if ctx.Err() != nil {
return ctx.Err()
return nil
})
if errOuter != nil {
return errOuter
}
printer.P("")
@@ -2,6 +2,8 @@ package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"hash/fnv"
"io"
"math/rand"
@@ -67,7 +69,7 @@ func TestRepairSnapshotsWithLostData(t *testing.T) {
// repository must be ok after removing the broken snapshots
testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
testListSnapshots(t, env.gopts, 2)
_, err := testRunCheckOutput(t, env.gopts, false)
_, _, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
@@ -96,7 +98,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) {
testRunRebuildIndex(t, env.gopts)
testRunRepairSnapshot(t, env.gopts, true)
testListSnapshots(t, env.gopts, 1)
_, err := testRunCheckOutput(t, env.gopts, false)
_, _, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
@@ -119,7 +121,7 @@ func TestRepairSnapshotsWithLostRootTree(t *testing.T) {
testRunRebuildIndex(t, env.gopts)
testRunRepairSnapshot(t, env.gopts, true)
testListSnapshots(t, env.gopts, 0)
_, err := testRunCheckOutput(t, env.gopts, false)
_, _, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
@@ -136,3 +138,38 @@ func TestRepairSnapshotsIntact(t *testing.T) {
rtest.Assert(t, reflect.DeepEqual(oldSnapshotIDs, snapshotIDs), "unexpected snapshot id mismatch %v vs. %v", oldSnapshotIDs, snapshotIDs)
testRunCheck(t, env.gopts)
}
func TestRepairSnapshotsBrokenSnapshots(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testRunInit(t, env.gopts)
// create backup
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, BackupOptions{}, env.gopts)
// create zero length file in snapshots/
// will fail with
// failed to load snapshot 1d204771: LoadRaw(<snapshot/1d20477115>): invalid data returned
handle, err := os.Create(filepath.Join(env.repo, "snapshots", "1d20477115fb872069a28a80ffb95a82cb8b1b1920de046a68c0195da63f30cf"))
rtest.OK(t, err)
rtest.OK(t, handle.Close())
// create some file with a correct sha256 name in snapshots/, will fail with
// failed to load snapshot abcd1234: ciphertext verification failed
contents := rtest.Random(1234567890123, 42)
sha256Contents := sha256.Sum256(contents)
target := hex.EncodeToString(sha256Contents[:])
rtest.OK(t, os.WriteFile(filepath.Join(env.repo, "snapshots", target), contents, 0o600))
// run repair snapshots
repairOpts := RepairOptions{Forget: true}
env.gopts.BackendTestHook = nil
_, err = withCaptureStdout(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runRepairSnapshots(ctx, gopts, repairOpts, []string{"1d204771", target[:8]}, gopts.Term)
})
rtest.OK(t, err)
// verify that there are no snapshot errors
testRunCheck(t, env.gopts)
}
+3 -3
View File
@@ -11,9 +11,9 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/filter"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/restorer"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
restoreui "github.com/restic/restic/internal/ui/restore"
"github.com/spf13/cobra"
@@ -167,7 +167,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options,
return err
}
progress := restoreui.NewProgress(printer, ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
progress := restoreui.NewProgress(printer, gopts.Quiet, gopts.JSON, term.CanUpdateStatus())
res := restorer.NewRestorer(repo, sn, restorer.Options{
DryRun: opts.DryRun,
Sparse: opts.Sparse,
@@ -281,7 +281,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options,
return nil
}
func getXattrSelectFilter(opts RestoreOptions, printer progress.Printer) (func(xattrName string) bool, error) {
func getXattrSelectFilter(opts RestoreOptions, printer restic.Printer) (func(xattrName string) bool, error) {
hasXattrExcludes := len(opts.ExcludeXattrPattern) > 0
hasXattrIncludes := len(opts.IncludeXattrPattern) > 0
+3 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
@@ -242,7 +243,7 @@ func TestRestore(t *testing.T) {
testRunInit(t, env.gopts)
for i := 0; i < 10; i++ {
for i := range 10 {
p := filepath.Join(env.testdata, fmt.Sprintf("foo/bar/testfile%v", i))
rtest.OK(t, os.MkdirAll(filepath.Dir(p), 0755))
rtest.OK(t, appendRandomData(p, uint(rand.Intn(2<<21))))
@@ -329,6 +330,7 @@ func TestRestoreLatest(t *testing.T) {
}
func TestRestoreWithPermissionFailure(t *testing.T) {
repository.TestInjectKey(t, restic.TestParseID("18493b1f93ad90b6bce7ed3afa93395a1f90e981f04145c03e5958cafa2ee33b"), `{"mac":{"k":"7ftgSq7jNM2HiGCyY9TYrg==","r":"o+1bB0wApwqoG7oAXOLyDw=="},"encrypt":"i57gVfyYp9HmXjzE0dSZyrkp2FN9LE75uWFjOuWze1M="}`)
env, cleanup := withTestEnvironment(t)
defer cleanup()
+13 -9
View File
@@ -129,7 +129,7 @@ func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) {
// be updated accordingly.
type rewriteFilterFunc func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error)
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.Snapshot, opts RewriteOptions, printer progress.Printer) (bool, error) {
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.Snapshot, opts RewriteOptions, printer restic.Printer) (bool, error) {
if sn.Tree == nil {
return false, errors.Errorf("snapshot %v has nil tree", sn.ID().Str())
}
@@ -191,7 +191,7 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.
}
func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *data.Snapshot,
filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string, printer progress.Printer,
filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string, printer restic.Printer,
keepEmptySnapshot bool) (bool, error) {
var filteredTree restic.ID
@@ -298,7 +298,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
return errors.Fatal("exclude and include patterns are mutually exclusive")
}
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
var (
repo *repository.Repository
@@ -327,7 +327,10 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
}
changedCount := 0
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) {
err = opts.SnapshotFilter.FindAll(ctx, snapshotLister, repo, args, func(_ string, sn *data.Snapshot, err error) error {
if err != nil {
return err
}
printer.P("\n%v", sn)
changed, err := rewriteSnapshot(ctx, repo, sn, opts, printer)
if err != nil {
@@ -336,9 +339,10 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
if changed {
changedCount++
}
}
if ctx.Err() != nil {
return ctx.Err()
return nil
})
if err != nil {
return err
}
printer.P("")
@@ -359,7 +363,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
return nil
}
func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer progress.Printer) (rewriteNode walker.NodeRewriteFunc, keepEmptyDirectory walker.NodeKeepEmptyDirectoryFunc) {
func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer restic.Printer) (rewriteNode walker.NodeRewriteFunc, keepEmptyDirectory walker.NodeKeepEmptyDirectoryFunc) {
inSelectByName := func(nodepath string, node *data.Node) bool {
for _, include := range includeByNameFuncs {
matched, childMayMatch := include(nodepath)
@@ -406,7 +410,7 @@ func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer
return rewriteNode, keepEmptyDirectory
}
func gatherExcludeFilters(excludeByNameFuncs []filter.RejectByNameFunc, printer progress.Printer) (rewriteNode walker.NodeRewriteFunc) {
func gatherExcludeFilters(excludeByNameFuncs []filter.RejectByNameFunc, printer restic.Printer) (rewriteNode walker.NodeRewriteFunc) {
exSelectByName := func(nodepath string) bool {
for _, reject := range excludeByNameFuncs {
if reject(nodepath) {
+5 -4
View File
@@ -13,6 +13,7 @@ import (
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
func testRunRewriteExclude(t testing.TB, gopts global.Options, excludes []string, forget bool, metadata snapshotMetadataArgs) {
@@ -42,7 +43,7 @@ func testLsOutputContainsCount(t testing.TB, gopts global.Options, lsOpts LsOpti
t.Helper()
out := testRunLsWithOpts(t, gopts, lsOpts, lsArgs)
count := 0
for _, line := range strings.Split(string(out), "\n") {
for line := range strings.SplitSeq(string(out), "\n") {
if strings.Contains(line, substring) {
count++
}
@@ -81,7 +82,7 @@ func getSnapshot(t testing.TB, snapshotID restic.ID, env *testEnvironment) *data
var snapshots []*data.Snapshot
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@@ -157,7 +158,7 @@ func testRewriteMetadata(t *testing.T, metadata snapshotMetadataArgs) {
var snapshots []*data.Snapshot
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@@ -205,7 +206,7 @@ func TestRewriteSnaphotSummary(t *testing.T) {
// replace snapshot by one without a summary
var oldSummary *data.SnapshotSummary
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/selfupdate"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@@ -84,7 +85,7 @@ func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts global.Opt
}
}
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
printer.P("writing restic to %v", opts.Output)
v, err := selfupdate.DownloadLatestStableRelease(ctx, opts.Output, global.Version, printer.P)
+11 -5
View File
@@ -13,6 +13,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -81,7 +82,7 @@ func (opts *SnapshotOptions) Finalize() error {
}
func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
return err
@@ -89,12 +90,17 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Option
defer unlock()
var snapshots data.Snapshots
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
err = opts.SnapshotFilter.FindAll(ctx, repo, repo, args, func(_ string, sn *data.Snapshot, err error) error {
if err != nil {
return err
}
snapshots = append(snapshots, sn)
return nil
})
if err != nil {
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
snapshotGroups, grouped, err := data.GroupSnapshots(snapshots, opts.GroupBy)
if err != nil {
return err
+35 -109
View File
@@ -7,18 +7,15 @@ import (
"fmt"
"path/filepath"
"strings"
"sync"
"time"
"github.com/restic/chunker"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/restorer"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
statsui "github.com/restic/restic/internal/ui/stats"
"github.com/restic/restic/internal/ui/table"
"github.com/restic/restic/internal/walker"
@@ -34,13 +31,13 @@ func newStatsCommand(globalOptions *global.Options) *cobra.Command {
Short: "Scan the repository and show basic statistics",
Long: `
The "stats" command walks one or multiple snapshots in a repository
and accumulates statistics about the data stored therein. It reports
and accumulates statistics about the data stored therein. It reports
on the number of unique files and their sizes, according to one of
the counting modes as given by the --mode flag.
It operates on all snapshots matching the selection criteria or all
snapshots if nothing is specified. The special snapshot ID "latest"
is also supported. Some modes make more sense over
is also supported. Some modes make more sense over
just a single snapshot, while others are useful across all snapshots,
depending on what you are trying to calculate.
@@ -104,7 +101,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
return err
}
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
@@ -137,17 +134,19 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
}
var snapshots data.Snapshots
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) {
err = opts.SnapshotFilter.FindAll(ctx, snapshotLister, repo, args, func(_ string, sn *data.Snapshot, err error) error {
if err != nil {
return err
}
snapshots = append(snapshots, sn)
return nil
})
if err != nil {
return err
}
statsProgress := newStatsProgress(term, !gopts.JSON, uint64(len(snapshots)))
updater := progress.NewUpdater(ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()), func(runtime time.Duration, final bool) {
statsProgress.printProgress(runtime, final)
})
defer updater.Done()
statsProgress := statsui.NewProgress(term, gopts.Quiet, gopts.JSON, uint64(len(snapshots)))
defer statsProgress.Done()
for _, sn := range snapshots {
err = statsWalkSnapshot(ctx, sn, repo, opts, stats, statsProgress)
@@ -162,20 +161,20 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
if opts.countMode == countModeRawData {
// the blob handles have been collected, but not yet counted
for blobHandle := range stats.blobs.Keys() {
pbs := repo.LookupBlob(blobHandle.Type, blobHandle.ID)
pbs := repo.LookupBlob(blobHandle)
if len(pbs) == 0 {
return fmt.Errorf("blob %v not found", blobHandle)
}
stats.TotalSize += uint64(pbs[0].Length)
stats.TotalSize += uint64(pbs[0].CiphertextLength())
if repo.Config().Version >= 2 {
stats.TotalUncompressedSize += uint64(crypto.CiphertextLength(int(pbs[0].DataLength())))
stats.TotalUncompressedSize += uint64(pbs[0].UncompressedCiphertextLength())
if pbs[0].IsCompressed() {
stats.TotalCompressedBlobsSize += uint64(pbs[0].Length)
stats.TotalCompressedBlobsUncompressedSize += uint64(crypto.CiphertextLength(int(pbs[0].DataLength())))
stats.TotalCompressedBlobsSize += uint64(pbs[0].CiphertextLength())
stats.TotalCompressedBlobsUncompressedSize += uint64(pbs[0].UncompressedCiphertextLength())
}
}
stats.TotalBlobCount++
statsProgress.update(0, 1, uint64(pbs[0].Length))
statsProgress.Update(0, 1, uint64(pbs[0].CiphertextLength()))
}
if stats.TotalCompressedBlobsSize > 0 {
stats.CompressionRatio = float64(stats.TotalCompressedBlobsUncompressedSize) / float64(stats.TotalCompressedBlobsSize)
@@ -186,7 +185,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
}
}
// stop progress bar to prevent mangled output
updater.Done()
statsProgress.Done()
if gopts.JSON {
err = json.NewEncoder(gopts.Term.OutputWriter()).Encode(stats)
@@ -221,8 +220,8 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
return nil
}
func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic.Loader, opts StatsOptions, stats *statsContainer, progress *statsProgress) error {
progress.processSnapshot()
func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic.Loader, opts StatsOptions, stats *statsContainer, sp *statsui.Progress) error {
sp.ProcessSnapshot()
if snapshot.Tree == nil {
return fmt.Errorf("snapshot %s has nil tree", snapshot.ID().Str())
}
@@ -232,12 +231,12 @@ func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic
if opts.countMode == countModeRawData {
// count just the sizes of unique blobs; we don't need to walk the tree
// ourselves in this case, since a nifty function does it for us
return data.FindUsedBlobs(ctx, repo, restic.IDs{*snapshot.Tree}, stats.blobs, nil)
return data.FindUsedBlobs(ctx, repo, restic.IDs{*snapshot.Tree}, stats.blobs, restic.NoopCounter)
}
hardLinkIndex := restorer.NewHardlinkIndex[struct{}]()
hardLinkIndex := data.NewHardlinkIndex[struct{}]()
err := walker.Walk(ctx, repo, *snapshot.Tree, walker.WalkVisitor{
ProcessNode: statsWalkTree(repo, opts, stats, hardLinkIndex, progress),
ProcessNode: statsWalkTree(repo, opts, stats, hardLinkIndex, sp),
})
if err != nil {
return fmt.Errorf("walking tree %s: %v", *snapshot.Tree, err)
@@ -246,7 +245,7 @@ func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic
return nil
}
func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer, hardLinkIndex *restorer.HardlinkIndex[struct{}], progress *statsProgress) walker.WalkFunc {
func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer, hardLinkIndex *data.HardlinkIndex[struct{}], progress *statsui.Progress) walker.WalkFunc {
return func(parentTreeID restic.ID, npath string, node *data.Node, nodeErr error) error {
if nodeErr != nil {
return nodeErr
@@ -254,7 +253,7 @@ func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer,
if node == nil {
return nil
}
progress.update(1, 0, uint64(node.Size))
progress.Update(1, 0, uint64(node.Size))
if opts.countMode == countModeUniqueFilesByContents || opts.countMode == countModeBlobsPerFile {
// only count this file if we haven't visited it before
fid := makeFileIDByContents(node)
@@ -274,14 +273,14 @@ func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer,
// ensure we have this file (by path) in our map; in this
// mode, a file is unique by both contents and path
nodePath := filepath.Join(npath, node.Name)
progress.update(0, 1, 0)
progress.Update(0, 1, 0)
if _, ok := stats.fileBlobs[nodePath]; !ok {
stats.fileBlobs[nodePath] = restic.NewIDSet()
stats.TotalFileCount++
}
if _, ok := stats.fileBlobs[nodePath][blobID]; !ok {
// is always a data blob since we're accessing it via a file's Content array
blobSize, found := repo.LookupBlobSize(restic.DataBlob, blobID)
blobSize, found := repo.LookupBlobSize(restic.BlobHandle{Type: restic.DataBlob, ID: blobID})
if !found {
return fmt.Errorf("blob %s not found for tree %s", blobID, parentTreeID)
}
@@ -371,76 +370,6 @@ type statsContainer struct {
// independent of references to files
blobs restic.AssociatedBlobSet
}
type statsProgress struct {
term ui.Terminal
m sync.Mutex
snapshotCount uint64
show bool
processedSnapshotCount uint64
processedFileCount uint64
processedBlobCount uint64
processedSize uint64
}
func newStatsProgress(term ui.Terminal, show bool, snapshotCount uint64) *statsProgress {
return &statsProgress{
term: term,
show: show,
snapshotCount: snapshotCount,
}
}
func (s *statsProgress) printProgress(runtime time.Duration, final bool) {
if !s.show {
return
}
s.m.Lock()
progressBase := s.processedSnapshotCount
if progressBase > 0 && !final {
progressBase--
}
status := fmt.Sprintf("[%s] %s %d / %d snapshots", ui.FormatDuration(runtime), ui.FormatPercent(progressBase, s.snapshotCount), s.processedSnapshotCount, s.snapshotCount)
if s.processedFileCount > 0 {
status += fmt.Sprintf(", %v files", s.processedFileCount)
}
if s.processedBlobCount > 0 {
status += fmt.Sprintf(", %d blobs", s.processedBlobCount)
}
status += fmt.Sprintf(", %s", ui.FormatBytes(s.processedSize))
s.m.Unlock()
if final {
s.term.SetStatus(nil)
s.term.Print(status)
} else {
s.term.SetStatus([]string{status})
}
}
func (s *statsProgress) update(fileCount uint64, blobCount uint64, size uint64) {
s.m.Lock()
defer s.m.Unlock()
s.processedFileCount += fileCount
s.processedBlobCount += blobCount
s.processedSize += size
}
func (s *statsProgress) processSnapshot() {
s.m.Lock()
defer s.m.Unlock()
s.processedSnapshotCount++
s.processedFileCount = 0
s.processedBlobCount = 0
s.processedSize = 0
}
// fileID is a 256-bit hash that distinguishes unique files.
type fileID [32]byte
@@ -453,7 +382,7 @@ const (
countModeDebug = "debug"
)
func statsDebug(ctx context.Context, repo restic.Repository, printer progress.Printer) error {
func statsDebug(ctx context.Context, repo restic.Repository, printer restic.Printer) error {
printer.E("Collecting size statistics\n\n")
for _, t := range []restic.FileType{restic.KeyFile, restic.LockFile, restic.IndexFile, restic.SnapshotFile, restic.PackFile} {
hist, err := statsDebugFileType(ctx, repo, t)
@@ -486,12 +415,12 @@ func statsDebugFileType(ctx context.Context, repo restic.Lister, tpe restic.File
func statsDebugBlobs(ctx context.Context, repo restic.Repository) ([restic.NumBlobTypes]*sizeHistogram, error) {
var hist [restic.NumBlobTypes]*sizeHistogram
for i := 0; i < len(hist); i++ {
for i := range len(hist) {
hist[i] = newSizeHistogram(2 * chunker.MaxSize)
}
err := repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
hist[pb.Type].Add(uint64(pb.Length))
err := repo.ListBlobs(ctx, func(pb restic.PackBlob) {
hist[pb.Handle().Type].Add(uint64(pb.CiphertextLength()))
})
return hist, err
@@ -517,10 +446,7 @@ func newSizeHistogram(sizeLimit uint64) *sizeHistogram {
growthFactor := uint64(10)
for lowerBound < sizeLimit {
upperBound := lowerBound*growthFactor - 1
if upperBound > sizeLimit {
upperBound = sizeLimit
}
upperBound := min(lowerBound*growthFactor-1, sizeLimit)
h.buckets = append(h.buckets, sizeClass{lowerBound, upperBound, 0})
lowerBound *= growthFactor
}
+1 -38
View File
@@ -2,10 +2,8 @@ package main
import (
"testing"
"time"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
)
func TestSizeHistogramNew(t *testing.T) {
@@ -26,7 +24,7 @@ func TestSizeHistogramNew(t *testing.T) {
func TestSizeHistogramAdd(t *testing.T) {
h := newSizeHistogram(42)
for i := uint64(0); i < 45; i++ {
for i := range uint64(45) {
h.Add(i)
}
@@ -62,38 +60,3 @@ func TestSizeHistogramString(t *testing.T) {
rtest.Equals(t, "Count: 3\nTotal Size: 11 B\nSize Count\n-------------------\n 0 - 0 Byte 1\n 1 - 9 Byte 1\n10 - 42 Byte 1\n-------------------\n", h.String())
})
}
func TestStatsProgress(t *testing.T) {
term := &ui.MockTerminal{}
progress := newStatsProgress(term, true, 2)
progress.printProgress(0*time.Second, false)
rtest.Equals(t, []string{"[0:00] 0.00% 0 / 2 snapshots, 0 B"}, term.Output)
progress.processSnapshot()
progress.update(1, 2, 3)
progress.printProgress(5*time.Second, false)
// Output differs from the previous one because the progress is based on the number of processed snapshots,
// 1/2 snapshots means processing the snapshot 1 currently
rtest.Equals(t, []string{"[0:05] 0.00% 1 / 2 snapshots, 1 files, 2 blobs, 3 B"}, term.Output)
progress.processSnapshot()
progress.printProgress(10*time.Second, false)
rtest.Equals(t, []string{"[0:10] 50.00% 2 / 2 snapshots, 0 B"}, term.Output)
progress.update(4, 5, 6)
progress.printProgress(15*time.Second, false)
rtest.Equals(t, []string{"[0:15] 50.00% 2 / 2 snapshots, 4 files, 5 blobs, 6 B"}, term.Output)
progress.printProgress(20*time.Second, true)
rtest.Equals(t, []string{"[0:20] 100.00% 2 / 2 snapshots, 4 files, 5 blobs, 6 B"}, term.Output)
}
func TestStatsProgressJSON(t *testing.T) {
term := &ui.MockTerminal{}
progress := newStatsProgress(term, false, 2)
progress.printProgress(0*time.Second, false)
// JSON output is not available yet, so just make sure to not break normal json output
rtest.Equals(t, nil, term.Output)
}
+11 -7
View File
@@ -13,6 +13,7 @@ import (
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
func newTagCommand(globalOptions *global.Options) *cobra.Command {
@@ -120,7 +121,7 @@ func changeTags(ctx context.Context, repo *repository.Repository, sn *data.Snaps
}
func runTag(ctx context.Context, opts TagOptions, gopts global.Options, term ui.Terminal, args []string) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 {
return errors.Fatal("nothing to do!")
@@ -158,19 +159,22 @@ func runTag(ctx context.Context, opts TagOptions, gopts global.Options, term ui.
}
}
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
err = opts.SnapshotFilter.FindAll(ctx, repo, repo, args, func(_ string, sn *data.Snapshot, err error) error {
if err != nil {
return err
}
changed, err := changeTags(ctx, repo, sn, opts.SetTags.Flatten(), opts.AddTags.Flatten(), opts.RemoveTags.Flatten(), printFunc)
if err != nil {
printer.E("unable to modify the tags for snapshot ID %q, ignoring: %v", sn.ID(), err)
continue
return nil
}
if changed {
summary.ChangedSnapshots++
}
}
if ctx.Err() != nil {
return ctx.Err()
return nil
})
if err != nil {
return err
}
printSummary(summary)
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@@ -47,7 +48,7 @@ func (opts *UnlockOptions) AddFlags(f *pflag.FlagSet) {
}
func runUnlock(ctx context.Context, opts UnlockOptions, gopts global.Options, term ui.Terminal) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
repo, err := global.OpenRepository(ctx, gopts, printer)
if err != nil {
return err
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"runtime"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra"
)
@@ -25,7 +25,7 @@ Exit status is 1 if there was any error.
`,
DisableAutoGenTag: true,
Run: func(_ *cobra.Command, _ []string) {
printer := ui.NewProgressPrinter(globalOptions.JSON, globalOptions.Verbosity, globalOptions.Term)
printer := progress.NewTerminalPrinter(globalOptions.JSON, globalOptions.Verbosity, globalOptions.Term)
if globalOptions.JSON {
type jsonVersion struct {
+1 -34
View File
@@ -1,17 +1,14 @@
package main
import (
"context"
"os"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/pflag"
)
// initMultiSnapshotFilter is used for commands that work on multiple snapshots
// MUST be combined with FindFilteredSnapshots
// MUST be combined with (*data,SnapshotFilter).FindAll
// MUST be followed by finalizeSnapshotFilter after flag parsing
func initMultiSnapshotFilter(flags *pflag.FlagSet, filt *data.SnapshotFilter, addHostShorthand bool) {
hostShorthand := "H"
@@ -47,33 +44,3 @@ func finalizeSnapshotFilter(filt *data.SnapshotFilter) {
filt.Hosts = nil
}
}
// FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots.
func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, f *data.SnapshotFilter, snapshotIDs []string, printer progress.Printer) <-chan *data.Snapshot {
out := make(chan *data.Snapshot)
go func() {
defer close(out)
be, err := restic.MemorizeList(ctx, be, restic.SnapshotFile)
if err != nil {
printer.E("could not load snapshots: %v", err)
return
}
err = f.FindAll(ctx, be, loader, snapshotIDs, func(id string, sn *data.Snapshot, err error) error {
if err != nil {
printer.E("Ignoring %q: %v", id, err)
} else {
select {
case <-ctx.Done():
return ctx.Err()
case out <- sn:
}
}
return nil
})
if err != nil {
printer.E("could not load snapshots: %v", err)
}
}()
return out
}
+9 -5
View File
@@ -9,9 +9,9 @@ import (
"github.com/restic/restic/internal/ui"
)
func formatNode(path string, n *data.Node, long bool, human bool) string {
func formatNodeOutput(n lsNodeOutput, long bool, human bool) string {
if !long {
return path
return n.Path
}
var mode os.FileMode
@@ -19,9 +19,9 @@ func formatNode(path string, n *data.Node, long bool, human bool) string {
var size string
if human {
size = ui.FormatBytes(n.Size)
size = ui.FormatBytes(n.fileSize())
} else {
size = fmt.Sprintf("%6d", n.Size)
size = fmt.Sprintf("%6d", n.fileSize())
}
switch n.Type {
@@ -44,6 +44,10 @@ func formatNode(path string, n *data.Node, long bool, human bool) string {
return fmt.Sprintf("%s %5d %5d %s %s %s%s",
mode|n.Mode, n.UID, n.GID, size,
n.ModTime.Local().Format(global.TimeFormat), path,
n.ModTime.Local().Format(global.TimeFormat), n.Path,
target)
}
func formatNode(path string, n *data.Node, long bool, human bool) string {
return formatNodeOutput(lsNodeOutputFrom(path, n), long, human)
}
+36 -40
View File
@@ -9,12 +9,13 @@ import (
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"testing"
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/all"
"github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/retry"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors"
@@ -23,7 +24,7 @@ import (
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/ui/termstatus"
)
@@ -193,6 +194,7 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
repository.TestUseLowSecurityKDFParameters(t)
restic.TestDisableCheckPolynomial(t)
retry.TestFastRetries(t)
layout.TestDisablePackSubdirs(t)
tempdir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-")
rtest.OK(t, err)
@@ -211,11 +213,12 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
rtest.OK(t, os.MkdirAll(env.repo, 0700))
env.gopts = global.Options{
Repo: env.repo,
Quiet: true,
CacheDir: env.cache,
Password: rtest.TestPassword,
Extended: make(options.Options),
Repo: env.repo,
Quiet: true,
CacheDir: env.cache,
Password: rtest.TestPassword,
Extended: make(options.Options),
Compression: repository.CompressionFastest,
// replace this hook with "nil" if listing a filetype more than once is necessary
BackendTestHook: func(r backend.Backend) (backend.Backend, error) { return newOrderedListOnceBackend(r), nil },
@@ -244,7 +247,7 @@ func testSetupBackupData(t testing.TB, env *testEnvironment) string {
func listPacks(gopts global.Options, t *testing.T) restic.IDSet {
var packs restic.IDSet
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@@ -263,16 +266,16 @@ func listPacks(gopts global.Options, t *testing.T) restic.IDSet {
func listTreePacks(gopts global.Options, t *testing.T) restic.IDSet {
var treePacks restic.IDSet
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
rtest.OK(t, r.LoadIndex(ctx, nil))
rtest.OK(t, r.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
treePacks = restic.NewIDSet()
return r.ListBlobs(ctx, func(pb restic.PackedBlob) {
if pb.Type == restic.TreeBlob {
treePacks.Insert(pb.PackID)
return r.ListBlobs(ctx, func(pb restic.PackBlob) {
if pb.Handle().Type == restic.TreeBlob {
treePacks.Insert(pb.PackID())
}
})
})
@@ -294,13 +297,13 @@ func captureBackend(gopts *global.Options) func() backend.Backend {
func removePacks(gopts global.Options, t testing.TB, remove restic.IDSet) {
be := captureBackend(&gopts)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
ctx, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
for id := range remove {
rtest.OK(t, be().Remove(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()}))
rtest.OK(t, be().Remove(ctx, backend.Handle{Type: backend.PackFile, Name: id.String()}))
}
return nil
})
@@ -310,18 +313,18 @@ func removePacks(gopts global.Options, t testing.TB, remove restic.IDSet) {
func removePacksExcept(gopts global.Options, t testing.TB, keep restic.IDSet, removeTreePacks bool) {
be := captureBackend(&gopts)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
ctx, r, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
// Get all tree packs
rtest.OK(t, r.LoadIndex(ctx, nil))
rtest.OK(t, r.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
treePacks := restic.NewIDSet()
rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackedBlob) {
if pb.Type == restic.TreeBlob {
treePacks.Insert(pb.PackID)
rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackBlob) {
if pb.Handle().Type == restic.TreeBlob {
treePacks.Insert(pb.PackID())
}
}))
@@ -330,20 +333,14 @@ func removePacksExcept(gopts global.Options, t testing.TB, keep restic.IDSet, re
if treePacks.Has(id) != removeTreePacks || keep.Has(id) {
return nil
}
return be().Remove(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()})
return be().Remove(ctx, backend.Handle{Type: backend.PackFile, Name: id.String()})
})
})
rtest.OK(t, err)
}
func includes(haystack []string, needle string) bool {
for _, s := range haystack {
if s == needle {
return true
}
}
return false
return slices.Contains(haystack, needle)
}
func loadSnapshotMap(t testing.TB, gopts global.Options) map[string]struct{} {
@@ -371,7 +368,7 @@ func lastSnapshot(old, new map[string]struct{}) (map[string]struct{}, string) {
func testLoadSnapshot(t testing.TB, gopts global.Options, id restic.ID) *data.Snapshot {
var snapshot *data.Snapshot
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@@ -421,6 +418,13 @@ func withCaptureStdout(t testing.TB, gopts global.Options, callback func(ctx con
return buf, err
}
func withCaptureStdoutStderr(t testing.TB, gopts global.Options, callback func(ctx context.Context, gopts global.Options) error) (*bytes.Buffer, *bytes.Buffer, error) {
bufStdout := bytes.NewBuffer(nil)
bufStderr := bytes.NewBuffer(nil)
err := withTermStatusRaw(os.Stdin, bufStdout, bufStderr, gopts, callback)
return bufStdout, bufStderr, err
}
func withTermStatus(t testing.TB, gopts global.Options, callback func(ctx context.Context, gopts global.Options) error) error {
// stdout and stderr are written to by printer functions etc. That is the written data
// usually consists of one or multiple lines and therefore can be handled well
@@ -429,18 +433,10 @@ func withTermStatus(t testing.TB, gopts global.Options, callback func(ctx contex
}
func withTermStatusRaw(stdin io.ReadCloser, stdout, stderr io.Writer, gopts global.Options, callback func(ctx context.Context, gopts global.Options) error) error {
ctx, cancel := context.WithCancel(context.TODO())
var wg sync.WaitGroup
term := termstatus.New(stdin, stdout, stderr, gopts.Quiet)
term, cancel := termstatus.Setup(stdin, stdout, stderr, gopts.Quiet)
gopts.Term = term
wg.Add(1)
go func() {
defer wg.Done()
term.Run(ctx)
}()
defer wg.Wait()
defer cancel()
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
return callback(ctx, gopts)
+30 -16
View File
@@ -12,12 +12,14 @@ import (
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
func TestCheckRestoreNoLock(t *testing.T) {
repository.TestInjectKey(t, restic.TestParseID("a19acdab068765b022ffb81cb5aac83c5de4bf4fbce0d26e9ade8e636c6ae49f"), `{"mac":{"k":"TbkpCBdNYAvAwb+64r8VGw==","r":"Q5V1CnAvBQREgJAOQD40Bw=="},"encrypt":"SjCkTpms+XOUJR5LSsy2G+uO9ngG7H0L+IVwPV4u70A="}`)
env, cleanup := withTestEnvironment(t)
defer cleanup()
@@ -46,14 +48,14 @@ func TestCheckRestoreNoLock(t *testing.T) {
// is expected by the first listing + some operations.
type listOnceBackend struct {
backend.Backend
listedFileType map[restic.FileType]bool
listedFileType map[backend.FileType]bool
strictOrder bool
}
func newListOnceBackend(be backend.Backend) *listOnceBackend {
return &listOnceBackend{
Backend: be,
listedFileType: make(map[restic.FileType]bool),
listedFileType: make(map[backend.FileType]bool),
strictOrder: false,
}
}
@@ -61,16 +63,16 @@ func newListOnceBackend(be backend.Backend) *listOnceBackend {
func newOrderedListOnceBackend(be backend.Backend) *listOnceBackend {
return &listOnceBackend{
Backend: be,
listedFileType: make(map[restic.FileType]bool),
listedFileType: make(map[backend.FileType]bool),
strictOrder: true,
}
}
func (be *listOnceBackend) List(ctx context.Context, t restic.FileType, fn func(backend.FileInfo) error) error {
if t != restic.LockFile && be.listedFileType[t] {
func (be *listOnceBackend) List(ctx context.Context, t backend.FileType, fn func(backend.FileInfo) error) error {
if t != backend.LockFile && be.listedFileType[t] {
return errors.Errorf("tried listing type %v the second time", t)
}
if be.strictOrder && t == restic.SnapshotFile && be.listedFileType[restic.IndexFile] {
if be.strictOrder && t == backend.SnapshotFile && be.listedFileType[backend.IndexFile] {
return errors.Errorf("tried listing type snapshots after index")
}
be.listedFileType[t] = true
@@ -165,20 +167,32 @@ func TestFindListOnce(t *testing.T) {
var snapshotIDs restic.IDSet
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
snapshotIDs = restic.NewIDSet()
// specify the two oldest snapshots explicitly and use "latest" to reference the newest one
for sn := range FindFilteredSnapshots(ctx, repo, repo, &data.SnapshotFilter{}, []string{
secondSnapshot[0].String(),
secondSnapshot[1].String()[:8],
"latest",
}, printer) {
snapshotIDs.Insert(*sn.ID())
err = (&data.SnapshotFilter{}).FindAll(ctx, repo, repo,
[]string{
secondSnapshot[0].String(),
secondSnapshot[1].String()[:8],
"latest",
},
func(id string, sn *data.Snapshot, err error) error {
if err != nil {
return err
}
snapshotIDs.Insert(*sn.ID())
return nil
})
if err != nil {
return err
}
return nil
}))
@@ -194,7 +208,7 @@ type failConfigOnceBackend struct {
func (be *failConfigOnceBackend) Load(ctx context.Context, h backend.Handle,
length int, offset int64, fn func(rd io.Reader) error) error {
if !be.failedOnce && h.Type == restic.ConfigFile {
if !be.failedOnce && h.Type == backend.ConfigFile {
be.failedOnce = true
return fmt.Errorf("oops")
}
@@ -202,7 +216,7 @@ func (be *failConfigOnceBackend) Load(ctx context.Context, h backend.Handle,
}
func (be *failConfigOnceBackend) Stat(ctx context.Context, h backend.Handle) (backend.FileInfo, error) {
if !be.failedOnce && h.Type == restic.ConfigFile {
if !be.failedOnce && h.Type == backend.ConfigFile {
be.failedOnce = true
return backend.FileInfo{}, fmt.Errorf("oops")
}
+6 -10
View File
@@ -5,10 +5,10 @@ import (
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/restic"
)
func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bool, exclusive bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) {
func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bool, exclusive bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) {
repo, err := global.OpenRepository(ctx, gopts, printer)
if err != nil {
return nil, nil, nil, err
@@ -16,9 +16,7 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
unlock := func() {}
if !dryRun {
var lock *repository.Unlocker
lock, ctx, err = repository.Lock(ctx, repo, exclusive, gopts.RetryLock, func(msg string) {
unlock, ctx, err = repository.LockRepo(ctx, repo, exclusive, gopts.RetryLock, func(msg string) {
if !gopts.JSON {
printer.P("%s", msg)
}
@@ -26,8 +24,6 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
if err != nil {
return nil, nil, nil, err
}
unlock = lock.Unlock
} else {
repo.SetDryRun()
}
@@ -35,18 +31,18 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
return ctx, repo, unlock, nil
}
func openWithReadLock(ctx context.Context, gopts global.Options, noLock bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) {
func openWithReadLock(ctx context.Context, gopts global.Options, noLock bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) {
// TODO enforce read-only operations once the locking code has moved to the repository
// As in-depth hardening, put the repository into read-only mode if noLock is true
// Not possible if the repository has to be locked.
return internalOpenWithLocked(ctx, gopts, noLock, false, printer)
}
func openWithAppendLock(ctx context.Context, gopts global.Options, dryRun bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) {
func openWithAppendLock(ctx context.Context, gopts global.Options, dryRun bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) {
// TODO enforce non-exclusive operations once the locking code has moved to the repository
return internalOpenWithLocked(ctx, gopts, dryRun, false, printer)
}
func openWithExclusiveLock(ctx context.Context, gopts global.Options, dryRun bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) {
func openWithExclusiveLock(ctx context.Context, gopts global.Options, dryRun bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) {
return internalOpenWithLocked(ctx, gopts, dryRun, true, printer)
}
+4 -5
View File
@@ -20,7 +20,6 @@ import (
"github.com/restic/restic/internal/feature"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/termstatus"
)
@@ -50,7 +49,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
PersistentPreRunE: func(c *cobra.Command, _ []string) error {
switch c.Name() {
case "__complete", "__completeNoDesc":
case cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
return nil
}
return globalOptions.PreRun(needsPassword(c.Name()))
@@ -118,7 +117,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
// user for authentication).
func needsPassword(cmd string) bool {
switch cmd {
case "cache", "generate", "help", "options", "self-update", "version", "__complete", "__completeNoDesc":
case "cache", "generate", "help", "options", "self-update", "version", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
return false
default:
return true
@@ -198,7 +197,7 @@ func main() {
var exitMessage string
switch {
case restic.IsAlreadyLocked(err):
case repository.IsAlreadyLocked(err):
exitMessage = fmt.Sprintf("%v\nthe `unlock` command can be used to remove stale locks", err)
case err == ErrInvalidSourceData:
exitMessage = fmt.Sprintf("Warning: %v", err)
@@ -228,7 +227,7 @@ func main() {
exitCode = 3
case errors.Is(err, global.ErrNoRepository):
exitCode = 10
case restic.IsAlreadyLocked(err):
case repository.IsAlreadyLocked(err):
exitCode = 11
case errors.Is(err, repository.ErrNoKeyFound):
exitCode = 12
-30
View File
@@ -281,36 +281,6 @@ after the bucket name like this:
be converted to path-style URLs instead, for example ``s3.us-west-2.amazonaws.com/bucket_name``.
See below for configuration options for S3-compatible storage from other providers.
Minio Server
************
`Minio <https://min.io/>`__ is an Open Source Object Storage,
written in Go and compatible with Amazon S3 API.
- Download and Install `Minio Download <https://min.io/download#/linux>`__.
- You can also refer to `Minio Docs <https://min.io/docs/minio/linux/>`__ for step by step guidance
on installation and getting started on Minio Client and Minio Server.
You must first setup the following environment variables with the
credentials of your Minio Server.
.. code-block:: console
$ export AWS_ACCESS_KEY_ID=<YOUR-MINIO-ACCESS-KEY-ID>
$ export AWS_SECRET_ACCESS_KEY=<YOUR-MINIO-SECRET-ACCESS-KEY>
Now you can easily initialize restic to use Minio server as a backend with
this command.
.. code-block:: console
$ restic -r s3:http://localhost:9000/restic init
enter password for new repository:
enter password again:
created restic repository 6ad29560f5 at s3:http://localhost:9000/restic
Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is irrecoverably lost.
S3-compatible storage
*********************
+11
View File
@@ -399,6 +399,9 @@ sign, write ``$$`` to the file - this has to be done even when there's no
matching environment variable for the word following a single ``$``. Note
that tilde (``~``) is not expanded, instead use the ``$HOME`` or equivalent
environment variable (depending on your operating system).
For environment variable names with non-alphanumeric characters,
enclose the name in curly braces (e.g., ``${ProgramFiles(x86)}``).
Without braces, the parser stops at the first non-alphanumeric character.
Patterns need to match on complete path components. For example, the pattern ``foo``:
@@ -494,6 +497,8 @@ By specifying the option ``--one-file-system`` you can instruct restic
to only backup files from the file systems the initially specified files
or directories reside on. In other words, it will prevent restic from crossing
filesystem boundaries and subvolumes when performing a backup.
Internally, restic identifies a file's filesystem by the "device ID"
that is returned by calling lstat() on the file.
For example, if you backup ``/`` with this option and you have external
media mounted under ``/media/usb`` then restic will not back up ``/media/usb``
@@ -657,6 +662,12 @@ If there is a **bind-mount** below a directory that is to be saved, restic desce
archived as a block device file and restored as such. This also means that the content of the
corresponding disk is not read, at least not from the device file.
**Socket files** are not backed up because they represent active connections by running processes
and are only meaningful if created by those processes.
**FIFO files** are backed up, but because their contents are volatile and never stored on disk,
only the file system entry and metadata will be backed up.
By default, restic does not save the access time (atime) for any files or other
items, since it is not possible to reliably disable updating the access time by
restic itself. This means that for each new backup a lot of metadata is
+6 -3
View File
@@ -92,9 +92,12 @@ By default, restic does not restore files as sparse. Use ``restore --sparse`` to
enable the creation of sparse files if supported by the filesystem. Then restic
will restore long runs of zero bytes as holes in the corresponding files.
Reading from a hole returns the original zero bytes, but it does not consume
disk space. Note that the exact location of the holes can differ from those in
the original file, as their location is determined while restoring and is not
stored explicitly.
disk space. Note that restic does not keep track of which files were originally
sparse or at what locations sparse holes were. When restoring, restic will create
a sparse hole (by seeking forward instead of writing data) for any range of zeros
at the start of a blob. This range can span the entire blob. Because large files
result in blobs much larger than file system blocks, restoring a large sparse file
with many holes may make it significantly larger than the original sparse file.
Restoring extended file attributes
----------------------------------
+3 -1
View File
@@ -323,7 +323,7 @@ five days that had a snapshot and remove the other snapshots:
keep 5 snapshots:
ID Time Host Tags Reasons Paths
-------------------------------------------------------------------------------
8a603ddf 2025-04-25 23:00:00 mopped daily snapshot /home/user/work
ae33bf10 2025-04-25 23:00:00 mopped daily snapshot /home/user/work
fb8b1418 2025-04-28 11:00:00 mopped daily snapshot /home/user/work
1db188b4 2025-04-29 11:00:00 mopped daily snapshot /home/user/work
7baa4f28 2025-05-01 11:00:00 mopped daily snapshot /home/user/work
@@ -507,6 +507,8 @@ The ``prune`` command accepts the following options:
- ``--verbose`` increased verbosity shows additional statistics for ``prune``.
- ``--json`` gives the statistics in JSON format.
Recovering from "no free space" errors
**************************************
+113 -2
View File
@@ -41,6 +41,8 @@ environment variables, which are listed below.
RESTIC_PROGRESS_FPS Frames per second by which the progress bar is updated
RESTIC_PACK_SIZE Target size for pack files
RESTIC_READ_CONCURRENCY Concurrency for file reads
RESTIC_IGNORE_CTIME Ignore ctime changes when comparing files (replaces --ignore-ctime)
RESTIC_IGNORE_INODE Ignore inode changes when comparing files (replaces --ignore-inode)
RESTIC_FROM_REPOSITORY Source repository for copy (replaces --from-repo)
RESTIC_FROM_REPOSITORY_FILE File containing source repository for copy (replaces --from-repository-file)
@@ -241,6 +243,13 @@ Several commands, in particular long running ones or those that generate a large
use a format also known as JSON lines. It consists of a stream of new-line separated JSON
messages. You can determine the nature of the message using the ``message_type`` field.
Messages with type ``status`` are emitted at a regular interval while the command runs,
by default about ten times per second, regardless of whether the output is a terminal
or a pipe. Passing ``--quiet`` disables them. The ``RESTIC_PROGRESS_FPS`` environment
variable overrides both: it sets the number of status updates per second and re-enables
them even when ``--quiet`` is set. Values below 1 are allowed, for example ``0.0166``
results in roughly one status message per minute, which is useful when capturing logs.
backup
------
@@ -311,6 +320,15 @@ Verbose status provides details about the progress, including details about back
| ``total_files`` | Total number of files | uint64 |
+---------------------------+----------------------------------------------------------+---------+
Excluded backup items are detailed in this format:
+---------------------------+----------------------------------------------------------+---------+
| ``message_type`` | Always "excluded_item" | string |
+---------------------------+----------------------------------------------------------+---------+
| ``item`` | The item in question | string |
+---------------------------+----------------------------------------------------------+---------+
Summary
^^^^^^^
@@ -534,8 +552,7 @@ forget
The ``forget`` command prints a single JSON document containing an array of
ForgetGroups. If specific snapshot IDs are specified, then no output is generated.
The ``prune`` command does not yet support JSON such that ``forget --prune``
results in a mix of JSON and text output.
When using the ``--prune`` option, its output is appended in the JSON lines format.
ForgetGroup
^^^^^^^^^^^
@@ -599,6 +616,100 @@ KeepReason object
+--------------+--------------------------------------------------------+--------------------+
prune
-----
The ``prune`` command uses the JSON lines format, but only outputs a single message.
+------------------+----------------------------------------+--------------------------+
| ``message_type`` | Always "summary" | string |
+------------------+----------------------------------------+--------------------------+
| ``blobs`` | Statistics regarding data blobs | `PruneBlobs object`_ |
+------------------+----------------------------------------+--------------------------+
| ``bytes`` | Statistics regarding sizes in bytes | `PruneSizes object`_ |
+------------------+----------------------------------------+--------------------------+
| ``packfiles`` | Statistics regarding packfiles | `PrunePackfiles object`_ |
+------------------+----------------------------------------+--------------------------+
.. _PruneBlobs object:
PruneBlobs object
+-----------------+----------------------------------------+------+
| ``used`` | Number of used blobs | uint |
+-----------------+----------------------------------------+------+
| ``duplicate`` | Number of duplicate blobs | uint |
+-----------------+----------------------------------------+------+
| ``unused`` | Number of unused blobs | uint |
+-----------------+----------------------------------------+------+
| ``total`` | Total number of blobs | uint |
+-----------------+----------------------------------------+------+
| ``repack`` | Number of blobs to be repacked | uint |
+-----------------+----------------------------------------+------+
| ``repack_remove`` | Number of blobs removed by repacking | uint |
+-----------------+----------------------------------------+------+
| ``remove`` | Number of blobs removed by pack deletion | uint |
+-----------------+----------------------------------------+------+
| ``remove_total`` | Total number of blobs to be removed | uint |
+-----------------+----------------------------------------+------+
| ``remaining`` | Number of blobs remaining | uint |
+-----------------+----------------------------------------+------+
.. _PruneSizes object:
PruneSizes object
+--------------------+-------------------------------------+--------+
| ``used`` | Size of used blobs | uint64 |
+--------------------+-------------------------------------+--------+
| ``duplicate`` | Size of duplicate blobs | uint64 |
+--------------------+-------------------------------------+--------+
| ``unused`` | Size of unused blobs | uint64 |
+--------------------+-------------------------------------+--------+
| ``unreferenced`` | Size of unreferenced pack files | uint64 |
+--------------------+-------------------------------------+--------+
| ``uncompressed`` | Size of uncompressed pack files | uint64 |
+--------------------+-------------------------------------+--------+
| ``total`` | Total size of blobs | uint64 |
+--------------------+-------------------------------------+--------+
| ``repack`` | Size of blobs to be repacked | uint64 |
+--------------------+-------------------------------------+--------+
| ``repack_remove`` | Size of blobs removed by repacking | uint64 |
+--------------------+-------------------------------------+--------+
| ``remove`` | Size of blobs removed by pack deletion | uint64 |
+--------------------+-------------------------------------+--------+
| ``remove_total`` | Total size of blobs to be removed | uint64 |
+--------------------+-------------------------------------+--------+
| ``remaining`` | Size of blobs remaining | uint64 |
+--------------------+-------------------------------------+--------+
| ``remaining_unused`` | Size of remaining unused blobs | uint64 |
+--------------------+-------------------------------------+--------+
.. _PrunePackfiles object:
PrunePackfiles object
+------------------+---------------------------------------+------+
| ``used`` | Number of used pack files | uint |
+------------------+---------------------------------------+------+
| ``unused`` | Number of unused pack files | uint |
+------------------+---------------------------------------+------+
| ``partly_used`` | Number of partially used pack files | uint |
+------------------+---------------------------------------+------+
| ``unreferenced`` | Number of unreferenced pack files | uint |
+------------------+---------------------------------------+------+
| ``total`` | Total number of pack files | uint |
+------------------+---------------------------------------+------+
| ``keep`` | Number of pack files to keep | uint |
+------------------+---------------------------------------+------+
| ``repack`` | Number of pack files to repack | uint |
+------------------+---------------------------------------+------+
| ``remove`` | Number of pack files to remove | uint |
+------------------+---------------------------------------+------+
| ``remove_total`` | Total number of pack files to remove | uint |
+------------------+---------------------------------------+------+
init
----
+40 -6
View File
@@ -80,11 +80,15 @@ Similarly, if a repository is repeatedly damaged, please open an `issue on GitHu
somewhere. Please include the check output and additional information that might
help locate the problem.
If ``check`` detects damaged pack files, it will show instructions on how to repair
When ``restic check`` detects damaged or missing packfiles, it will show instructions on how to repair
them using the ``repair packs`` command. Use that command instead of the "Repairing the
index" section in this guide.
If you are interested to check only specific snapshots, you can now
If ``check`` detects unreadable snapshot files, it will show instructions on how to repair
them using the ``repair snapshots`` command. Follow those instructions as part of the
"Removing broken snapshots" section in this guide.
If you are interested to check only specific snapshots, you can
use the standard snapshot filter method specifying ``--host``, ``--path``, ``--tag`` or
alternatively naming snapshot ID(s) explicitly. The selected subset of packfiles
will then be checked for consistency and read when either ``--read-data`` or
@@ -148,8 +152,39 @@ repair the index first!
Please note that it is not recommended to repair the index unless the repository
is actually damaged.
4. Removing broken snapshots
****************************
4. Running all backups (optional)
.. note::
This step is only necessary if the ``check`` command tells you to run ``restic repair snapshots``.
In case of damage to a snapshot file, ``check`` will show an error message like the following:
.. code-block:: console
$ restic check
using temporary cache in /tmp/restic-check-cache-2150939789
create exclusive lock for repository
repository cfabc5ed opened (version 1)
created new cache in /tmp/restic-check-cache-2150939789
load indexes
[0:00] 100.00% 1 / 1 index files loaded
check all packs
check snapshots, trees and blobs
error: failed to load snapshot 1d204771: LoadRaw(<snapshot/1d20477115>): invalid data returned
[0:00] 100.00% 1 / 1 snapshots
The repository contains damaged snapshot files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.
restic repair snapshots --forget 1d2047711588c657efea246369c499bb2133240b1e03477d503386ceaa92fa2f
Damaged snapshot files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!
Fatal: repository contains errors
As explained in the command output, you have to run ``restic repair snapshots --forget 1d2047711588c657efea246369c499bb2133240b1e03477d503386ceaa92fa2f`` to remove the broken snapshot file.
5. Running all backups (optional)
*********************************
With a correct index, the ``backup`` command guarantees that newly created
@@ -165,7 +200,7 @@ To check if the repository is fully repaired, you can run ``restic check``
To get a list of still damaged files, you can run ``restic repair snapshots --dry-run``.
Look for ``would save new snapshot`` messages to find affected snapshots.
5. Removing missing data from snapshots
6. Removing missing data from snapshots
***************************************
If your repository is still missing data, then you can use the ``repair snapshots``
@@ -190,8 +225,7 @@ If you did not add the ``--forget`` option, then you have to manually delete all
modified snapshots using the ``forget`` command. In the example above, you'd have
to run ``restic forget 6979421e``.
6. Checking the repository again
7. Checking the repository again
********************************
As a final step, run ``check`` again to make sure that the repository has been successfully
+3 -2
View File
@@ -184,7 +184,8 @@ default backup, to be found at ``cmd/restic/testdata/backup-data.tar.gz``.
In this compressed archive you will find files, hardlinked files,
symlinked files, an empty directory and a simple directory structure which is good for testing purposes.
Commands that require a ``progress.Printer`` should either be wrapped in ``withTermStatus`` or ``withCaptureStdout``.
Commands that require a ``restic.Printer`` should either be wrapped in ``withTermStatus`` or ``withCaptureStdout``.
The interface is defined in ``internal/restic``; ``progress.NewTerminalPrinter`` returns a ``restic.Printer``.
If you want to analyze JSON output, you use ``withCaptureStdout()``.
It returns the generated output in a ``*bytes.Buffer``.
JSON output can be unmarshalled to produce the appropriate go structures; see
@@ -236,7 +237,7 @@ Security
possible critical security problem, please do *not* open a GitHub issue
but send an email directly to alexander@bumpern.de. If possible, please
encrypt your email using the following PGP key
(`0x91A6868BD3F7A907 <https://pgp.mit.edu/pks/lookup?op=get&search=0xCF8F18F2844575973F79D4E191A6868BD3F7A907>`__):
(`0x91A6868BD3F7A907 <https://restic.net/gpg-key-alex.asc>`__):
::
+1
View File
@@ -17,6 +17,7 @@ Talks
The following talks will be or have been given about restic:
- 2026-06-10: `restic on the FLOSS Weekly podcast <https://hackaday.com/2026/06/10/floss-weekly-episode-870-open-source-gardening/>`__
- 2021-04-02: `The Changelog: Restic has your backup
(Podcast) <https://changelog.com/podcast/434>`__
- 2016-01-31: Lightning Talk at the Go Devroom at FOSDEM 2016,
+7 -6
View File
@@ -47,12 +47,13 @@ comparing its output to the file name. If the prefix of a filename is
unique amongst all the other files in the same directory, the prefix may
be used instead of the complete filename.
Apart from the files stored within the ``keys`` and ``data`` directories,
all files are encrypted with AES-256 in counter mode (CTR). The integrity
of the encrypted data is secured by a Poly1305-AES message authentication
code (MAC).
Files in the ``data`` directory ("pack files") consist of multiple parts
which are all independently encrypted and authenticated, see below.
Apart from the files stored within the ``keys`` directory, all files
are encrypted with AES-256 in counter mode (CTR). The integrity of the
encrypted data is secured by a
Poly1305-AES message authentication code (MAC).
Files in the ``data`` directory ("pack files") consist of multiple
independently encrypted and authenticated parts: the individual blobs as
well as the pack header, see below.
In the first 16 bytes of each encrypted file the initialisation vector
(IV) is stored. It is followed by the encrypted data and completed by
+1
View File
@@ -291,6 +291,7 @@ Archive** storage classes is available:
- Currently, only the following commands are known to work:
- ``backup``
- ``check``
- ``copy``
- ``prune``
- ``restore``
+14 -1
View File
@@ -30,7 +30,8 @@ The allowed types are (in alphabetic order):
- :ref:`list_index`
- :ref:`keys <list_keys_locks_packs>`
- :ref:`locks <list_keys_locks_packs>`
- :ref:`packs <list_keys_locks_packs>`
- :ref:`packs<list_keys_locks_packs>`
- :ref:`packs [snapshot ID]<list_keys_locks_packs>`
- :ref:`list_snapshots`
@@ -160,6 +161,18 @@ Here is an example which lists all the packs in the repository:
953e5381138bdc44da23740a83065809dd4021f45ce4e351b577dc4c07f81314
75bca8556f47d16362e58e757ea89a34b28fb96aedcc314bea35d468e5cb665c
If you want to list all packfiles which are part of a snapshot, use the command
``list packs`` and attach the snapshot ID of the snapshot you want to see.
.. code-block:: console
$ restic -r /srv/restic-repo list packs latest -q
ae92415f79c281330159ed590db2a973048c886aacfa4fabfa0eaac10b396b8f
dc25893d422a71af41aaaa843cd7121708cd88679bf3885f94a32900ac068e84
This will give you the list of all packfiles, sorted in ID ascending order.
.. _view-repository-objects:
Inspecting repository objects
+35 -35
View File
@@ -9,10 +9,10 @@ toolchain go1.25.10
godebug winsymlink=0
require (
cloud.google.com/go/storage v1.62.2
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0
cloud.google.com/go/storage v1.64.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0
github.com/Backblaze/blazer v0.7.2
github.com/Microsoft/go-winio v0.6.2
github.com/anacrolix/fuse v0.3.1
@@ -22,42 +22,42 @@ require (
github.com/go-ole/go-ole v1.3.0
github.com/google/go-cmp v0.7.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/klauspost/compress v1.18.6
github.com/minio/minio-go/v7 v7.1.0
github.com/klauspost/compress v1.18.7
github.com/minio/minio-go/v7 v7.2.1
github.com/ncw/swift/v2 v2.0.5
github.com/peterbourgon/unixtransport v0.0.7
github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.7.0
github.com/pkg/sftp v1.13.10
github.com/pkg/sftp v1.13.11
github.com/pkg/xattr v0.4.12
github.com/restic/chunker v0.4.0
github.com/restic/chunker v0.5.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
go.uber.org/automaxprocs v1.6.0
golang.org/x/crypto v0.52.0
golang.org/x/net v0.55.0
golang.org/x/crypto v0.54.0
golang.org/x/net v0.57.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
golang.org/x/text v0.37.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0
golang.org/x/text v0.40.0
golang.org/x/time v0.15.0
google.golang.org/api v0.282.0
google.golang.org/api v0.291.0
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth v0.22.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.7.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cloud.google.com/go/iam v1.11.0 // indirect
cloud.google.com/go/monitoring v1.29.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -65,7 +65,6 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/felixge/fgprof v0.9.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -73,8 +72,8 @@ require (
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
github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
@@ -91,18 +90,19 @@ require (
github.com/tinylib/msgp v1.6.1 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.2 // indirect
)
+86 -82
View File
@@ -2,50 +2,50 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ=
cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4=
cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k=
cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI=
cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY=
cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM=
cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg=
cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ=
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0 h1:BM85pSYlVYQHdq00nxyPoOkyLF5NArJG3bOsrmbwr4k=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0/go.mod h1:QYjP2cB7ZYtS/8jAbE0VSBZde/tjExqGjp+8JY6/+ts=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0 h1:irsmOWwkp0KCTTNS5e2hdFeIvSQClQo2No3IaNmL3Vw=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0/go.mod h1:GWcBkQj3MqN7ozHKLaCCAuNLiXoIGv2RtanfAwSjY/Y=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM=
github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
@@ -93,8 +93,6 @@ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNu
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -120,10 +118,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
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.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw=
github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE=
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/googleapis/enterprise-certificate-proxy v0.3.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4=
github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
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=
@@ -131,8 +129,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -155,8 +153,8 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8=
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/ncw/swift/v2 v2.0.5 h1:9o5Gsd7bInAFEqsGPcaUdsboMbqf8lnNtxqWKFT9iz8=
github.com/ncw/swift/v2 v2.0.5/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
@@ -173,8 +171,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=
github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
@@ -184,8 +182,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/restic/chunker v0.4.0 h1:YUPYCUn70MYP7VO4yllypp2SjmsRhRJaad3xKu1QFRw=
github.com/restic/chunker v0.4.0/go.mod h1:z0cH2BejpW636LXw0R/BGyv+Ey8+m9QGiOanDHItzyw=
github.com/restic/chunker v0.5.0 h1:1y+ut0MBduzxODJ298rhQCtESoEpj8v1hTydZlKaE1Y=
github.com/restic/chunker v0.5.0/go.mod h1:z0cH2BejpW636LXw0R/BGyv+Ey8+m9QGiOanDHItzyw=
github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
@@ -207,11 +205,13 @@ github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9C
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
@@ -226,24 +226,26 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -251,8 +253,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
@@ -265,16 +267,16 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -288,20 +290,20 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
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=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -314,16 +316,16 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I=
google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/api v0.291.0 h1:wfPbbY+mr9c7wZLqqzrHJLft/q8iFKREd6IgTBUene0=
google.golang.org/api v0.291.0/go.mod h1:at7kwWbuonglBFEBoeMDAV1bguHqL3qf0BHFsv3coa0=
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 h1:YJjbgu+dkp5kUJLfpMyCLfBIWZb/FcJyuLeo1gVBOuo=
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -332,6 +334,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+5 -8
View File
@@ -39,7 +39,7 @@ func init() {
pflag.Parse()
}
func die(f string, args ...interface{}) {
func die(f string, args ...any) {
if !strings.HasSuffix(f, "\n") {
f += "\n"
}
@@ -48,7 +48,7 @@ func die(f string, args ...interface{}) {
os.Exit(1)
}
func msg(f string, args ...interface{}) {
func msg(f string, args ...any) {
if !strings.HasSuffix(f, "\n") {
f += "\n"
}
@@ -56,7 +56,7 @@ func msg(f string, args ...interface{}) {
fmt.Printf(f, args...)
}
func verbose(f string, args ...interface{}) {
func verbose(f string, args ...any) {
if !opts.Verbose {
return
}
@@ -207,10 +207,7 @@ func buildForTarget(sourceDir, outputDir, goos, goarch string) (filename string)
func buildTargets(sourceDir, outputDir string, targets map[string][]string) {
start := time.Now()
// the go compiler is already parallelized, thus reduce the concurrency a bit
workers := runtime.GOMAXPROCS(0) / 4
if workers < 1 {
workers = 1
}
workers := max(runtime.GOMAXPROCS(0)/4, 1)
msg("building with %d workers", workers)
type Job struct{ GOOS, GOARCH string }
@@ -218,7 +215,7 @@ func buildTargets(sourceDir, outputDir string, targets map[string][]string) {
var wg errgroup.Group
ch := make(chan Job)
for i := 0; i < workers; i++ {
for range workers {
wg.Go(func() error {
for job := range ch {
start := time.Now()
+5 -5
View File
@@ -43,7 +43,7 @@ func init() {
pflag.Parse()
}
func die(f string, args ...interface{}) {
func die(f string, args ...any) {
if !strings.HasSuffix(f, "\n") {
f += "\n"
}
@@ -52,7 +52,7 @@ func die(f string, args ...interface{}) {
os.Exit(1)
}
func msg(f string, args ...interface{}) {
func msg(f string, args ...any) {
if !strings.HasSuffix(f, "\n") {
f += "\n"
}
@@ -428,11 +428,11 @@ func updateDocker(sourceDir, version string) string {
buildCmd := fmt.Sprintf("docker buildx build --builder %s --platform linux/386,linux/amd64,linux/arm,linux/arm64 --pull -f docker/Dockerfile.release %q", builderName, sourceDir)
run("sh", "-c", buildCmd+" --no-cache")
publishCmds := ""
var publishCmds strings.Builder
for _, tag := range []string{"restic/restic:latest", "restic/restic:" + version} {
publishCmds += buildCmd + fmt.Sprintf(" --tag %q --push\n", tag)
publishCmds.WriteString(buildCmd + fmt.Sprintf(" --tag %q --push\n", tag))
}
return publishCmds + "\ndocker buildx rm " + builderName
return publishCmds.String() + "\ndocker buildx rm " + builderName
}
func tempdir(prefix string) string {
+41 -40
View File
@@ -67,8 +67,8 @@ func (s *ItemStats) Add(other ItemStats) {
s.TreeSizeInRepo += other.TreeSizeInRepo
}
// ToNoder returns a data.Node for a File.
type ToNoder interface {
// toNoder returns a data.Node for a File.
type toNoder interface {
ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*data.Node, error)
}
@@ -77,7 +77,7 @@ type archiverRepo interface {
restic.WithBlobUploader
restic.SaverUnpacked[restic.WriteableFileType]
Config() restic.Config
ChunkerFactory() restic.ChunkerFactory
}
// Archiver saves a directory structure to the repo.
@@ -110,11 +110,11 @@ type Archiver struct {
// particular file/dir.
//
// Once reading a file has completed successfully (but not saving it yet),
// CompleteItem will be called with current == nil.
// CompleteItem will be called with a zero ItemAction.
//
// CompleteItem may be called asynchronously from several different
// goroutines!
CompleteItem func(item string, previous, current *data.Node, s ItemStats, d time.Duration)
CompleteItem func(item string, action ItemAction, s ItemStats, d time.Duration)
// StartFile is called when a file is being processed by a worker.
StartFile func(filename string)
@@ -129,6 +129,9 @@ type Archiver struct {
// Flags controlling change detection. See doc/040_backup.rst for details.
ChangeIgnoreFlags uint
// for excluded items
ExcludedItem func(path string)
}
// Flags for the ChangeIgnoreFlags bitfield.
@@ -149,9 +152,9 @@ type Options struct {
SaveTreeConcurrency uint
}
// ApplyDefaults returns a copy of o with the default options set for all unset
// applyDefaults returns a copy of o with the default options set for all unset
// fields.
func (o Options) ApplyDefaults() Options {
func (o Options) applyDefaults() Options {
if o.ReadConcurrency == 0 {
// two is a sweet spot for almost all situations. We've done some
// experiments documented here:
@@ -178,11 +181,12 @@ func New(repo archiverRepo, filesystem fs.FS, opts Options) *Archiver {
SelectByName: func(_ string) bool { return true },
Select: func(_ string, _ *fs.ExtendedFileInfo, _ fs.FS) bool { return true },
FS: filesystem,
Options: opts.ApplyDefaults(),
Options: opts.applyDefaults(),
CompleteItem: func(string, *data.Node, *data.Node, ItemStats, time.Duration) {},
CompleteItem: func(string, ItemAction, ItemStats, time.Duration) {},
StartFile: func(string) {},
CompleteBlob: func(uint64) {},
ExcludedItem: func(string) {},
}
return arch
@@ -211,45 +215,40 @@ func (arch *Archiver) error(item string, err error) error {
}
func (arch *Archiver) trackItem(item string, previous, current *data.Node, s ItemStats, d time.Duration) {
arch.CompleteItem(item, previous, current, s, d)
action := itemProgressAction(previous, current)
arch.CompleteItem(item, action, s, d)
arch.mu.Lock()
defer arch.mu.Unlock()
arch.summary.ItemStats.Add(s)
if current != nil {
arch.summary.ProcessedBytes += current.Size
} else {
if action.IsZero() {
// last item or an error occurred
return
}
switch current.Type {
case data.NodeTypeDir:
switch {
case previous == nil:
arch.summary.Dirs.New++
case previous.Equals(*current):
arch.summary.Dirs.Unchanged++
default:
arch.summary.Dirs.Changed++
}
arch.summary.ProcessedBytes += current.Size
case data.NodeTypeFile:
switch {
case previous == nil:
arch.summary.Files.New++
case previous.Equals(*current):
arch.summary.Files.Unchanged++
default:
arch.summary.Files.Changed++
}
switch action {
case ActionDirNew:
arch.summary.Dirs.New++
case ActionDirUnchanged:
arch.summary.Dirs.Unchanged++
case ActionDirModified:
arch.summary.Dirs.Changed++
case ActionFileNew:
arch.summary.Files.New++
case ActionFileUnchanged:
arch.summary.Files.Unchanged++
case ActionFileModified:
arch.summary.Files.Changed++
}
}
// nodeFromFileInfo returns the restic node from an os.FileInfo.
func (arch *Archiver) nodeFromFileInfo(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*data.Node, error) {
func (arch *Archiver) nodeFromFileInfo(snPath, filename string, meta toNoder, ignoreXattrListError bool) (*data.Node, error) {
node, err := meta.ToNode(ignoreXattrListError, func(format string, args ...any) {
_ = arch.error(filename, fmt.Errorf(format, args...))
})
@@ -297,7 +296,7 @@ func (arch *Archiver) loadSubtree(ctx context.Context, node *data.Node) (data.Tr
}
func (arch *Archiver) wrapLoadTreeError(id restic.ID, err error) error {
if _, ok := arch.Repo.LookupBlobSize(restic.TreeBlob, id); ok {
if _, ok := arch.Repo.LookupBlobSize(restic.BlobHandle{Type: restic.TreeBlob, ID: id}); ok {
err = errors.Errorf("tree %v could not be loaded; the repository could be damaged: %v", id, err)
} else {
err = errors.Errorf("tree %v is not known; the repository could be damaged, run `repair index` to try to repair it", id)
@@ -444,7 +443,7 @@ func (fn *futureNode) take(ctx context.Context) futureNodeResult {
func (arch *Archiver) allBlobsPresent(previous *data.Node) bool {
// check if all blobs are contained in index
for _, id := range previous.Content {
if _, ok := arch.Repo.LookupBlobSize(restic.DataBlob, id); !ok {
if _, ok := arch.Repo.LookupBlobSize(restic.BlobHandle{Type: restic.DataBlob, ID: id}); !ok {
return false
}
}
@@ -486,6 +485,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
// exclude files by path before running Lstat to reduce number of lstat calls
if !explicit && !arch.SelectByName(abstarget) {
debug.Log("%v is excluded by path", target)
arch.ExcludedItem(abstarget)
return futureNode{}, true, nil
}
@@ -514,6 +514,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
}
if !explicit && !arch.Select(abstarget, fi, arch.FS) {
debug.Log("%v is excluded", target)
arch.ExcludedItem(abstarget)
return futureNode{}, true, nil
}
@@ -790,9 +791,9 @@ func (arch *Archiver) dirPathToNode(snPath, target string) (node *data.Node, err
//
// Paths returned with Explicit true are those the user listed literally; paths
// inserted from directory expansion have Explicit false.
func resolveRelativeTargets(filesys fs.FS, targets []string) ([]BackupTarget, error) {
func resolveRelativeTargets(filesys fs.FS, targets []string) ([]backupTarget, error) {
debug.Log("targets before resolving: %v", targets)
result := make([]BackupTarget, 0, len(targets))
result := make([]backupTarget, 0, len(targets))
for _, target := range targets {
if target != "" && filesys.VolumeName(target) == target {
// special case to allow users to also specify a volume name "C:" instead of a path "C:\"
@@ -802,7 +803,7 @@ func resolveRelativeTargets(filesys fs.FS, targets []string) ([]BackupTarget, er
}
pc, _ := pathComponents(filesys, target, false)
if len(pc) > 0 {
result = append(result, BackupTarget{Path: target, Explicit: true})
result = append(result, backupTarget{Path: target, Explicit: true})
continue
}
@@ -814,7 +815,7 @@ func resolveRelativeTargets(filesys fs.FS, targets []string) ([]BackupTarget, er
sort.Strings(entries)
for _, name := range entries {
result = append(result, BackupTarget{
result = append(result, backupTarget{
Path: filesys.Join(target, name),
Explicit: false,
})
@@ -863,7 +864,7 @@ func (arch *Archiver) loadParentTree(ctx context.Context, sn *data.Snapshot) dat
func (arch *Archiver) runWorkers(ctx context.Context, wg *errgroup.Group, uploader restic.BlobSaverAsync) {
arch.fileSaver = newFileSaver(ctx, wg,
uploader,
arch.Repo.Config().ChunkerPolynomial,
arch.Repo.ChunkerFactory(),
arch.Options.ReadConcurrency)
arch.fileSaver.CompleteBlob = arch.CompleteBlob
arch.fileSaver.NodeFromFileInfo = arch.nodeFromFileInfo
+45 -64
View File
@@ -132,11 +132,10 @@ func TestArchiverSaveFile(t *testing.T) {
for _, testfile := range tests {
t.Run("", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir, repo := prepareTempdirRepoSrc(t, TestDir{"file": testfile})
node, stats := saveFile(t, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.Local{}})
node, stats := saveFile(t, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.NewLocal()})
TestEnsureFileContent(ctx, t, repo, "file", node, testfile)
if stats.DataSize != uint64(len(testfile.Content)) {
@@ -165,8 +164,7 @@ func TestArchiverSaveFileReaderFS(t *testing.T) {
for _, test := range tests {
t.Run("", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
repo := repository.TestRepository(t)
@@ -206,12 +204,11 @@ func TestArchiverSave(t *testing.T) {
for _, testfile := range tests {
t.Run("", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir, repo := prepareTempdirRepoSrc(t, TestDir{"file": testfile})
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
arch.Error = func(item string, err error) error {
t.Errorf("archiver error for %v: %v", item, err)
return err
@@ -276,8 +273,7 @@ func TestArchiverSaveReaderFS(t *testing.T) {
for _, test := range tests {
t.Run("", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
repo := repository.TestRepository(t)
@@ -357,7 +353,7 @@ func BenchmarkArchiverSaveFileSmall(b *testing.B) {
tempdir, repo := prepareTempdirRepoSrc(b, d)
b.StartTimer()
_, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.Local{}})
_, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.NewLocal()})
b.StopTimer()
if stats.DataSize != fileSize {
@@ -389,7 +385,7 @@ func BenchmarkArchiverSaveFileLarge(b *testing.B) {
tempdir, repo := prepareTempdirRepoSrc(b, d)
b.StartTimer()
_, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.Local{}})
_, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.NewLocal()})
b.StopTimer()
if stats.DataSize != fileSize {
@@ -477,9 +473,9 @@ func TestArchiverSaveFileIncremental(t *testing.T) {
data := rtest.Random(23, 512*1024+887898)
testfile := filepath.Join(tempdir, "testfile")
for i := 0; i < 3; i++ {
for i := range 3 {
appendToFile(t, testfile, data)
node, _ := saveFile(t, repo, testfile, fs.Track{FS: fs.Local{}})
node, _ := saveFile(t, repo, testfile, fs.Track{FS: fs.NewLocal()})
t.Logf("node blobs: %v", node.Content)
@@ -572,13 +568,7 @@ func nodeFromFile(t testing.TB, localFs fs.FS, filename string) *data.Node {
// sleep sleeps long enough to ensure a timestamp change.
func sleep() {
d := 50 * time.Millisecond
if runtime.GOOS == "darwin" {
// On older Darwin instances, the file system only supports one second
// granularity.
d = 1500 * time.Millisecond
}
time.Sleep(d)
time.Sleep(5 * time.Millisecond)
}
func TestFileChanged(t *testing.T) {
@@ -691,7 +681,7 @@ func TestFileChanged(t *testing.T) {
}
save(t, filename, content)
fs := &fs.Local{}
fs := fs.NewLocal()
fiBefore, err := fs.Lstat(filename)
rtest.OK(t, err)
node := nodeFromFile(t, fs, filename)
@@ -735,7 +725,7 @@ func TestFilChangedSpecialCases(t *testing.T) {
t.Run("type-change", func(t *testing.T) {
fi := lstat(t, filename)
node := nodeFromFile(t, &fs.Local{}, filename)
node := nodeFromFile(t, fs.NewLocal(), filename)
node.Type = data.NodeTypeSymlink
if !fileChanged(fi, node, 0) {
t.Fatal("node with changed type detected as unchanged")
@@ -754,12 +744,12 @@ func TestArchiverSaveDir(t *testing.T) {
}{
{
src: TestDir{
"targetfile": TestFile{Content: string(rtest.Random(888, 2*1024*1024+5000))},
"targetfile": TestFile{Content: string(rtest.Random(888, 20*1024+5000))},
},
target: ".",
want: TestDir{
"targetdir": TestDir{
"targetfile": TestFile{Content: string(rtest.Random(888, 2*1024*1024+5000))},
"targetfile": TestFile{Content: string(rtest.Random(888, 20*1024+5000))},
},
},
},
@@ -769,8 +759,8 @@ func TestArchiverSaveDir(t *testing.T) {
"foo": TestFile{Content: "foo"},
"emptyfile": TestFile{Content: ""},
"bar": TestFile{Content: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
"largefile": TestFile{Content: string(rtest.Random(888, 2*1024*1024+5000))},
"largerfile": TestFile{Content: string(rtest.Random(234, 5*1024*1024+5000))},
"largefile": TestFile{Content: string(rtest.Random(888, 1*1024*1024+5000))},
"largerfile": TestFile{Content: string(rtest.Random(234, 3*1024*1024+5000))},
},
},
target: "targetdir",
@@ -837,7 +827,7 @@ func TestArchiverSaveDir(t *testing.T) {
t.Run("", func(t *testing.T) {
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
testFS := fs.Track{FS: fs.Local{}}
testFS := fs.Track{FS: fs.NewLocal()}
arch := New(repo, testFS, Options{})
arch.summary = &Summary{}
@@ -931,7 +921,7 @@ func TestArchiverSaveDirDuplicateExcludedEntry(t *testing.T) {
tempdir, repo := prepareTempdirRepoSrc(t, src)
testFS := fs.Track{FS: &duplicateReaddirFS{
FS: &fs.Local{},
FS: fs.NewLocal(),
dir: ".",
names: []string{"excluded", "excluded", "keep"},
}}
@@ -990,8 +980,8 @@ func TestArchiverSaveDirIncremental(t *testing.T) {
// save the empty directory several times in a row, then have a look if the
// archiver did save the same tree several times
for i := 0; i < 5; i++ {
testFS := fs.Track{FS: fs.Local{}}
for i := range 5 {
testFS := fs.Track{FS: fs.NewLocal()}
arch := New(repo, testFS, Options{})
arch.summary = &Summary{}
@@ -1173,7 +1163,7 @@ func TestArchiverSaveTree(t *testing.T) {
t.Run("", func(t *testing.T) {
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
testFS := fs.Track{FS: fs.Local{}}
testFS := fs.Track{FS: fs.NewLocal()}
arch := New(repo, testFS, Options{})
arch.summary = &Summary{}
@@ -1467,12 +1457,11 @@ func TestArchiverSnapshot(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
chdir := tempdir
if test.chdir != "" {
@@ -1571,7 +1560,7 @@ func TestResolveRelativeTargetsSpecial(t *testing.T) {
t.Skip("skip test on unix")
}
targets, err := resolveRelativeTargets(&fs.Local{}, test.targets)
targets, err := resolveRelativeTargets(fs.NewLocal(), test.targets)
rtest.OK(t, err)
paths := make([]string, len(targets))
for i, tgt := range targets {
@@ -1688,12 +1677,11 @@ func TestArchiverSnapshotSelect(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
arch.Select = test.selFn
back := rtest.Chdir(t, tempdir)
@@ -1796,12 +1784,11 @@ func TestArchiverExplicitBackupTarget(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
arch.Select = test.selFn
back := rtest.Chdir(t, tempdir)
@@ -1962,13 +1949,12 @@ func TestArchiverParent(t *testing.T) {
for _, test := range tests {
t.Run("", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
testFS := &MockFS{
FS: fs.Track{FS: fs.Local{}},
FS: fs.Track{FS: fs.NewLocal()},
bytesRead: make(map[string]int),
}
@@ -1988,7 +1974,7 @@ func TestArchiverParent(t *testing.T) {
t.Logf("testfs: %v", testFS)
// check that all files have been read exactly once
TestWalkFiles(t, ".", test.src, func(filename string, item interface{}) error {
TestWalkFiles(t, ".", test.src, func(filename string, item any) error {
file, ok := item.(TestFile)
if !ok {
return nil
@@ -2150,8 +2136,7 @@ func TestArchiverErrorReporting(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
@@ -2162,7 +2147,7 @@ func TestArchiverErrorReporting(t *testing.T) {
test.prepare(t)
}
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
arch.Error = test.errFn
target := test.targets
@@ -2243,7 +2228,7 @@ func TestArchiverContextCanceled(t *testing.T) {
back := rtest.Chdir(t, tempdir)
defer back()
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
_, snapshotID, _, err := arch.Snapshot(ctx, []string{"."}, SnapshotOptions{Time: time.Now()})
@@ -2376,8 +2361,7 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
for _, test := range tests {
t.Run("", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
@@ -2385,7 +2369,7 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
defer back()
testFS := &TrackFS{
FS: fs.Track{FS: fs.Local{}},
FS: fs.Track{FS: fs.NewLocal()},
opened: make(map[string]uint),
}
@@ -2518,7 +2502,7 @@ func TestMetadataChanged(t *testing.T) {
// get metadata
fi := lstat(t, "testfile")
localFS := &fs.Local{}
localFS := fs.NewLocal()
meta, err := localFS.OpenFile("testfile", fs.O_NOFOLLOW, true)
rtest.OK(t, err)
want, err := meta.ToNode(false, t.Logf)
@@ -2619,13 +2603,12 @@ func TestRacyFileTypeSwap(t *testing.T) {
tempfile := filepath.Join(tempdir, realName)
statfs := &overrideFS{
FS: fs.Local{},
FS: fs.NewLocal(),
overrideFI: fakeFI,
resetFIOnRead: true,
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
_ = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
wg, ctx := errgroup.WithContext(ctx)
@@ -2666,7 +2649,7 @@ func TestMetadataBackupErrorFiltering(t *testing.T) {
filename := filepath.Join(tempdir, "file")
repo := repository.TestRepository(t)
arch := New(repo, fs.Local{}, Options{})
arch := New(repo, fs.NewLocal(), Options{})
var filteredErr error
replacementErr := fmt.Errorf("replacement")
@@ -2715,7 +2698,7 @@ func TestIrregularFile(t *testing.T) {
fi.Mode = (fi.Mode &^ os.ModeType) | os.ModeIrregular
override := &overrideFS{
FS: fs.Local{},
FS: fs.NewLocal(),
overrideFI: fi,
overrideNode: &data.Node{
Type: data.NodeTypeIrregular,
@@ -2723,8 +2706,7 @@ func TestIrregularFile(t *testing.T) {
overrideErr: fmt.Errorf(`unsupported file type "irregular"`),
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
arch := New(repo, fs.Track{FS: override}, Options{})
_, excluded, err := arch.save(ctx, "/", tempfile, nil, false)
@@ -2770,13 +2752,12 @@ func TestDisappearedFile(t *testing.T) {
back := rtest.Chdir(t, tempdir)
defer back()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
// depending on the underlying FS implementation a missing file may be detected by OpenFile or
// the subsequent file.Stat() call. Thus test both cases.
for _, errorOnOpen := range []bool{false, true} {
arch := New(repo, fs.Track{FS: &missingFS{FS: &fs.Local{}, errorOnOpen: errorOnOpen}}, Options{})
arch := New(repo, fs.Track{FS: &missingFS{FS: fs.NewLocal(), errorOnOpen: errorOnOpen}}, Options{})
_, excluded, err := arch.save(ctx, "/", filepath.Join(tempdir, "testdir"), nil, false)
rtest.OK(t, err)
rtest.Assert(t, excluded, "testfile should have been excluded")
+2 -2
View File
@@ -12,8 +12,8 @@ import (
)
func statAndSnapshot(t *testing.T, repo archiverRepo, name string) (*data.Node, *data.Node) {
want := nodeFromFile(t, &fs.Local{}, name)
_, node := snapshot(t, repo, &fs.Local{}, nil, name)
want := nodeFromFile(t, fs.NewLocal(), name)
_, node := snapshot(t, repo, fs.NewLocal(), nil, name)
return want, node
}
+2 -2
View File
@@ -7,7 +7,7 @@ import (
func TestBufferPoolReuse(t *testing.T) {
success := false
// retries to avoid flakiness. The test can fail depending on the GC.
for i := 0; i < 100; i++ {
for range 100 {
// Test that buffers are actually reused from the pool
pool := newBufferPool(1024)
@@ -33,7 +33,7 @@ func TestBufferPoolReuse(t *testing.T) {
func TestBufferPoolLargeBuffers(t *testing.T) {
success := false
// retries to avoid flakiness. The test can fail depending on the GC.
for i := 0; i < 100; i++ {
for range 100 {
// Test that buffers larger than defaultSize are not returned to pool
pool := newBufferPool(1024)
buf := pool.Get()
+4 -4
View File
@@ -88,7 +88,7 @@ func (rc *rejectionCache) Store(dir string, rejected bool) {
// non-nil if the filename component of excludeFileSpec is empty. If rc is
// non-nil, it is going to be used in the RejectByNameFunc to expedite the evaluation
// of a directory based on previous visits.
func RejectIfPresent(excludeFileSpec string, warnf func(msg string, args ...interface{})) (RejectFunc, error) {
func RejectIfPresent(excludeFileSpec string, warnf func(msg string, args ...any)) (RejectFunc, error) {
if excludeFileSpec == "" {
return nil, errors.New("name for exclusion tagfile is empty")
}
@@ -115,7 +115,7 @@ func RejectIfPresent(excludeFileSpec string, warnf func(msg string, args ...inte
// tagfile which bears the name specified in tagFilename and starts with
// header. If rc is non-nil, it is used to expedite the evaluation of a
// directory based on previous visits.
func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache, fs fs.FS, warnf func(msg string, args ...interface{})) bool {
func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache, fs fs.FS, warnf func(msg string, args ...any)) bool {
if tagFilename == "" {
return false
}
@@ -136,7 +136,7 @@ func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache,
return rejected
}
func isDirExcludedByFile(dir, tagFilename, header string, fsInst fs.FS, warnf func(msg string, args ...interface{})) bool {
func isDirExcludedByFile(dir, tagFilename, header string, fsInst fs.FS, warnf func(msg string, args ...any)) bool {
tf := fsInst.Join(dir, tagFilename)
_, err := fsInst.Lstat(tf)
if errors.Is(err, os.ErrNotExist) {
@@ -318,7 +318,7 @@ func RejectBySize(maxSize int64) (RejectFunc, error) {
}
// RejectCloudFiles returns a func which rejects files which are online-only cloud files
func RejectCloudFiles(warnf func(msg string, args ...interface{})) (RejectFunc, error) {
func RejectCloudFiles(warnf func(msg string, args ...any)) (RejectFunc, error) {
return func(item string, fi *fs.ExtendedFileInfo, _ fs.FS) bool {
recall, err := fi.RecallOnDataAccess()
if err != nil {
+4 -4
View File
@@ -49,7 +49,7 @@ func TestIsExcludedByFile(t *testing.T) {
if tc.content == "" {
h = ""
}
if got := isExcludedByFile(foo, tagFilename, h, newRejectionCache(), &fs.Local{}, func(msg string, args ...interface{}) { t.Logf(msg, args...) }); tc.want != got {
if got := isExcludedByFile(foo, tagFilename, h, newRejectionCache(), fs.NewLocal(), func(msg string, args ...any) { t.Logf(msg, args...) }); tc.want != got {
t.Fatalf("expected %v, got %v", tc.want, got)
}
})
@@ -111,8 +111,8 @@ func TestMultipleIsExcludedByFile(t *testing.T) {
if err != nil {
return err
}
excludedByFoo := fooExclude(p, nil, &fs.Local{})
excludedByBar := barExclude(p, nil, &fs.Local{})
excludedByFoo := fooExclude(p, nil, fs.NewLocal())
excludedByBar := barExclude(p, nil, fs.NewLocal())
excluded := excludedByFoo || excludedByBar
// the log message helps debugging in case the test fails
t.Logf("%q: %v || %v = %v", p, excludedByFoo, excludedByBar, excluded)
@@ -243,7 +243,7 @@ func TestDeviceMap(t *testing.T) {
for _, test := range tests {
t.Run("", func(t *testing.T) {
res, err := deviceMap.IsAllowed(filepath.FromSlash(test.item), test.deviceID, &fs.Local{})
res, err := deviceMap.IsAllowed(filepath.FromSlash(test.item), test.deviceID, fs.NewLocal())
if err != nil {
t.Fatal(err)
}
+78 -21
View File
@@ -6,7 +6,6 @@ import (
"io"
"sync"
"github.com/restic/chunker"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
@@ -15,36 +14,38 @@ import (
"golang.org/x/sync/errgroup"
)
const chunkReadBufSize = 512 * 1024 // matches chunker internal read buffer size
// fileSaver concurrently saves incoming files to the repo.
type fileSaver struct {
saveFilePool *bufferPool
uploader restic.BlobSaverAsync
pol chunker.Pol
chunkerFactory restic.ChunkerFactory
ch chan<- saveFileJob
CompleteBlob func(bytes uint64)
NodeFromFileInfo func(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*data.Node, error)
NodeFromFileInfo func(snPath, filename string, meta toNoder, ignoreXattrListError bool) (*data.Node, error)
}
// newFileSaver returns a new file saver. A worker pool with fileWorkers is
// started, it is stopped when ctx is cancelled.
func newFileSaver(ctx context.Context, wg *errgroup.Group, uploader restic.BlobSaverAsync, pol chunker.Pol, fileWorkers uint) *fileSaver {
func newFileSaver(ctx context.Context, wg *errgroup.Group, uploader restic.BlobSaverAsync, chunkerFactory restic.ChunkerFactory, fileWorkers uint) *fileSaver {
ch := make(chan saveFileJob)
debug.Log("new file saver with %v file workers", fileWorkers)
s := &fileSaver{
uploader: uploader,
saveFilePool: newBufferPool(chunker.MaxSize),
pol: pol,
ch: ch,
uploader: uploader,
saveFilePool: newBufferPool(chunkerFactory.MaxChunkSize()),
chunkerFactory: chunkerFactory,
ch: ch,
CompleteBlob: func(uint64) {},
}
for i := uint(0); i < fileWorkers; i++ {
for range fileWorkers {
wg.Go(func() error {
s.worker(ctx, ch)
return nil
@@ -100,8 +101,63 @@ type saveFileJob struct {
complete fileCompleteFunc
}
type fileChunkState struct {
readBuf []byte
bpos uint
bmax uint
closed bool
}
func (s *fileChunkState) reset() {
s.bpos = 0
s.bmax = 0
s.closed = false
}
// readNextChunk reads from rd and returns the next chunk of data. io.EOF is
// returned when all chunks have been read.
func (s *fileChunkState) readNextChunk(rd io.Reader, chnker restic.Chunker, data []byte) ([]byte, error) {
data = data[:0]
for {
if s.bpos >= s.bmax {
n, err := io.ReadFull(rd, s.readBuf)
if err == io.ErrUnexpectedEOF {
err = nil
}
// io.EOF only happens when the end of the file has been reached.
// If this is the case, we need to return the data we have read so far.
if err == io.EOF && !s.closed {
s.closed = true
if len(data) > 0 {
return data, nil
}
}
if err != nil {
return nil, err
}
s.bpos = 0
s.bmax = uint(n)
}
split := chnker.NextSplitPoint(s.readBuf[s.bpos:s.bmax])
if split == -1 {
data = append(data, s.readBuf[s.bpos:s.bmax]...)
s.bpos = s.bmax
} else {
data = append(data, s.readBuf[s.bpos:s.bpos+uint(split)]...)
s.bpos += uint(split)
return data, nil
}
}
}
// saveFile stores the file f in the repo, then closes it.
func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPath string, target string, f fs.File, start func(), finishReading func(), finish func(res futureNodeResult)) {
func (s *fileSaver) saveFile(ctx context.Context, chnker restic.Chunker, chunkState *fileChunkState, snPath string, target string, f fs.File, start func(), finishReading func(), finish func(res futureNodeResult)) {
start()
fnr := futureNodeResult{
@@ -161,15 +217,15 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
return
}
// reuse the chunker
chnker.Reset(f, s.pol)
chnker.Reset()
chunkState.reset()
node.Content = []restic.ID{}
node.Size = 0
var idx int
for {
buf := s.saveFilePool.Get()
chunk, err := chnker.Next(buf.Data)
chunkData, err := chunkState.readNextChunk(f, chnker, buf.Data)
if err == io.EOF {
buf.Release()
break
@@ -181,8 +237,9 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
return
}
buf.Data = chunk.Data
node.Size += uint64(chunk.Length)
// put result buffer back for later reuse
buf.Data = chunkData
node.Size += uint64(len(chunkData))
// test if the context has been cancelled, return the error
if ctx.Err() != nil {
@@ -199,7 +256,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
node.Content = append(node.Content, restic.ID{})
lock.Unlock()
s.uploader.SaveBlobAsync(ctx, restic.DataBlob, buf.Data, restic.ID{}, false, func(newID restic.ID, known bool, sizeInRepo int, err error) {
s.uploader.SaveBlobAsync(ctx, restic.DataBlob, chunkData, restic.ID{}, false, func(newID restic.ID, known bool, sizeInRepo int, err error) {
defer buf.Release()
if err != nil {
completeError(err)
@@ -209,7 +266,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
lock.Lock()
if !known {
fnr.stats.DataBlobs++
fnr.stats.DataSize += uint64(len(buf.Data))
fnr.stats.DataSize += uint64(len(chunkData))
fnr.stats.DataSizeInRepo += uint64(sizeInRepo)
}
node.Content[pos] = newID
@@ -226,7 +283,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
return
}
s.CompleteBlob(uint64(len(chunk.Data)))
s.CompleteBlob(uint64(len(chunkData)))
}
err = f.Close()
@@ -246,8 +303,8 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
}
func (s *fileSaver) worker(ctx context.Context, jobs <-chan saveFileJob) {
// a worker has one chunker which is reused for each file (because it contains a rather large buffer)
chnker := chunker.New(nil, s.pol)
chnker := s.chunkerFactory.NewChunker()
chunkState := &fileChunkState{readBuf: make([]byte, chunkReadBufSize)}
for {
var job saveFileJob
@@ -261,7 +318,7 @@ func (s *fileSaver) worker(ctx context.Context, jobs <-chan saveFileJob) {
}
}
s.saveFile(ctx, chnker, job.snPath, job.target, job.file, job.start, func() {
s.saveFile(ctx, chnker, chunkState, job.snPath, job.target, job.file, job.start, func() {
if job.completeReading != nil {
job.completeReading()
}
+7 -11
View File
@@ -8,9 +8,9 @@ import (
"runtime"
"testing"
"github.com/restic/chunker"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/test"
"golang.org/x/sync/errgroup"
)
@@ -18,7 +18,7 @@ import (
func createTestFiles(t testing.TB, num int) (files []string) {
tempdir := test.TempDir(t)
for i := 0; i < num; i++ {
for i := range num {
filename := fmt.Sprintf("testfile-%d", i)
err := os.WriteFile(filepath.Join(tempdir, filename), []byte(filename), 0600)
if err != nil {
@@ -34,14 +34,11 @@ func startFileSaver(ctx context.Context, t testing.TB, _ fs.FS) (*fileSaver, *mo
wg, ctx := errgroup.WithContext(ctx)
workers := uint(runtime.NumCPU())
pol, err := chunker.RandomPolynomial()
if err != nil {
t.Fatal(err)
}
chunkerFactory := repository.TestRepository(t).ChunkerFactory()
saver := &mockSaver{saved: make(map[string]int)}
s := newFileSaver(ctx, wg, saver, pol, workers)
s.NodeFromFileInfo = func(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*data.Node, error) {
s := newFileSaver(ctx, wg, saver, chunkerFactory, workers)
s.NodeFromFileInfo = func(snPath, filename string, meta toNoder, ignoreXattrListError bool) (*data.Node, error) {
return meta.ToNode(ignoreXattrListError, t.Logf)
}
@@ -49,15 +46,14 @@ func startFileSaver(ctx context.Context, t testing.TB, _ fs.FS) (*fileSaver, *mo
}
func TestFileSaver(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
startFn := func() {}
completeReadingFn := func() {}
completeFn := func(*data.Node, ItemStats) {}
files := createTestFiles(t, 15)
testFs := fs.Local{}
testFs := fs.NewLocal()
s, saver, ctx, wg := startFileSaver(ctx, t, testFs)
var results []futureNode
+53
View File
@@ -0,0 +1,53 @@
package archiver
import "github.com/restic/restic/internal/data"
// ItemAction describes backup progress for a single file or directory.
// The zero value indicates an error or incomplete item.
type ItemAction string
// Constants for the different CompleteItem actions.
const (
ActionDirNew ItemAction = "dir new"
ActionDirUnchanged ItemAction = "dir unchanged"
ActionDirModified ItemAction = "dir modified"
ActionFileNew ItemAction = "file new"
ActionFileUnchanged ItemAction = "file unchanged"
ActionFileModified ItemAction = "file modified"
)
// IsZero reports whether the action describes a zero value.
func (a ItemAction) IsZero() bool {
return a == ""
}
func itemProgressAction(previous, current *data.Node) ItemAction {
if current == nil {
return ""
}
switch current.Type {
case data.NodeTypeDir:
switch {
case previous == nil:
return ActionDirNew
case previous.Equals(*current):
return ActionDirUnchanged
default:
return ActionDirModified
}
case data.NodeTypeFile:
switch {
case previous == nil:
return ActionFileNew
case previous.Equals(*current):
return ActionFileUnchanged
default:
return ActionFileModified
}
default:
return ""
}
}
+5 -7
View File
@@ -78,8 +78,7 @@ func TestScanner(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir := rtest.TempDir(t)
TestCreateFiles(t, tempdir, test.src)
@@ -92,7 +91,7 @@ func TestScanner(t *testing.T) {
t.Fatal(err)
}
sc := NewScanner(fs.Track{FS: fs.Local{}})
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
if test.selFn != nil {
sc.Select = test.selFn
}
@@ -213,8 +212,7 @@ func TestScannerError(t *testing.T) {
t.Skipf("skip on windows")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir := rtest.TempDir(t)
TestCreateFiles(t, tempdir, test.src)
@@ -231,7 +229,7 @@ func TestScannerError(t *testing.T) {
test.prepare(t)
}
sc := NewScanner(fs.Track{FS: fs.Local{}})
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
if test.selFn != nil {
sc.Select = test.selFn
}
@@ -299,7 +297,7 @@ func TestScannerCancel(t *testing.T) {
t.Fatal(err)
}
sc := NewScanner(fs.Track{FS: fs.Local{}})
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
var lastStats ScanStats
sc.Result = func(item string, s ScanStats) {
lastStats = s
+5 -5
View File
@@ -20,7 +20,7 @@ import (
// TestSnapshot creates a new snapshot of path.
func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *restic.ID) *data.Snapshot {
arch := New(repo, fs.Local{}, Options{})
arch := New(repo, fs.NewLocal(), Options{})
opts := SnapshotOptions{
Time: time.Now(),
Hostname: "localhost",
@@ -41,7 +41,7 @@ func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *res
}
// TestDir describes a directory structure to create for a test.
type TestDir map[string]interface{}
type TestDir map[string]any
func (d TestDir) String() string {
return "<Dir>"
@@ -119,7 +119,7 @@ func TestCreateFiles(t testing.TB, target string, dir TestDir) {
// TestWalkFunc is used by TestWalkFiles to traverse the dir. When an error is
// returned, traversal stops and the surrounding test is marked as failed.
type TestWalkFunc func(path string, item interface{}) error
type TestWalkFunc func(path string, item any) error
// TestWalkFiles runs fn for each file/directory in dir, the filename will be
// constructed with target as the prefix. Symlinks on Windows are ignored.
@@ -158,7 +158,7 @@ func TestEnsureFiles(t testing.TB, target string, dir TestDir) {
pathsChecked := make(map[string]struct{})
// first, test that all items are there
TestWalkFiles(t, target, dir, func(path string, item interface{}) error {
TestWalkFiles(t, target, dir, func(path string, item any) error {
fi, err := os.Lstat(path)
if err != nil {
return err
@@ -243,7 +243,7 @@ func TestEnsureFileContent(ctx context.Context, t testing.TB, repo restic.BlobLo
content := make([]byte, len(file.Content))
pos := 0
for _, id := range node.Content {
part, err := repo.LoadBlob(ctx, restic.DataBlob, id, content[pos:])
part, err := repo.LoadBlob(ctx, restic.BlobHandle{Type: restic.DataBlob, ID: id}, content[pos:])
if err != nil {
t.Fatalf("error loading blob %v: %v", id.Str(), err)
return
+29 -31
View File
@@ -1,7 +1,6 @@
package archiver
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -28,30 +27,30 @@ func (t *MockT) Fail() {
}
// Fatal is equivalent to Log followed by FailNow.
func (t *MockT) Fatal(args ...interface{}) {
func (t *MockT) Fatal(args ...any) {
t.T.Logf("MockT Fatal called with %v", args)
t.HasFailed = true
}
// Fatalf is equivalent to Logf followed by FailNow.
func (t *MockT) Fatalf(msg string, args ...interface{}) {
func (t *MockT) Fatalf(msg string, args ...any) {
t.T.Logf("MockT Fatal called: "+msg, args...)
t.HasFailed = true
}
// Error is equivalent to Log followed by Fail.
func (t *MockT) Error(args ...interface{}) {
func (t *MockT) Error(args ...any) {
t.T.Logf("MockT Error called with %v", args)
t.HasFailed = true
}
// Errorf is equivalent to Logf followed by Fail.
func (t *MockT) Errorf(msg string, args ...interface{}) {
func (t *MockT) Errorf(msg string, args ...any) {
t.T.Logf("MockT Error called: "+msg, args...)
t.HasFailed = true
}
func createFilesAt(t testing.TB, targetdir string, files map[string]interface{}) {
func createFilesAt(t testing.TB, targetdir string, files map[string]any) {
for name, item := range files {
target := filepath.Join(targetdir, filepath.FromSlash(name))
err := os.MkdirAll(filepath.Dir(target), 0700)
@@ -77,7 +76,7 @@ func createFilesAt(t testing.TB, targetdir string, files map[string]interface{})
func TestTestCreateFiles(t *testing.T) {
var tests = []struct {
dir TestDir
files map[string]interface{}
files map[string]any
}{
{
dir: TestDir{
@@ -91,7 +90,7 @@ func TestTestCreateFiles(t *testing.T) {
},
},
},
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
"subdir": TestDir{},
"subdir/subfile": TestFile{Content: "bar"},
@@ -196,7 +195,7 @@ func TestTestWalkFiles(t *testing.T) {
got := make(map[string]string)
TestCreateFiles(t, tempdir, test.dir)
TestWalkFiles(t, tempdir, test.dir, func(path string, item interface{}) error {
TestWalkFiles(t, tempdir, test.dir, func(path string, item any) error {
p, err := filepath.Rel(tempdir, path)
if err != nil {
return err
@@ -216,11 +215,11 @@ func TestTestWalkFiles(t *testing.T) {
func TestTestEnsureFiles(t *testing.T) {
var tests = []struct {
expectFailure bool
files map[string]interface{}
files map[string]any
want TestDir
}{
{
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
"subdir/subfile": TestFile{Content: "bar"},
"x/y/link": TestSymlink{Target: filepath.Clean("../../foo")},
@@ -239,7 +238,7 @@ func TestTestEnsureFiles(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
},
want: TestDir{
@@ -251,7 +250,7 @@ func TestTestEnsureFiles(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
"subdir/subfile": TestFile{Content: "bar"},
},
@@ -261,7 +260,7 @@ func TestTestEnsureFiles(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "xxx"},
},
want: TestDir{
@@ -270,7 +269,7 @@ func TestTestEnsureFiles(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestSymlink{Target: "/xxx"},
},
want: TestDir{
@@ -279,7 +278,7 @@ func TestTestEnsureFiles(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
},
want: TestDir{
@@ -288,7 +287,7 @@ func TestTestEnsureFiles(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestSymlink{Target: "xxx"},
},
want: TestDir{
@@ -297,7 +296,7 @@ func TestTestEnsureFiles(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestDir{
"foo": TestFile{Content: "foo"},
},
@@ -308,7 +307,7 @@ func TestTestEnsureFiles(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
},
want: TestDir{
@@ -341,11 +340,11 @@ func TestTestEnsureFiles(t *testing.T) {
func TestTestEnsureSnapshot(t *testing.T) {
var tests = []struct {
expectFailure bool
files map[string]interface{}
files map[string]any
want TestDir
}{
{
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
filepath.FromSlash("subdir/subfile"): TestFile{Content: "bar"},
filepath.FromSlash("x/y/link"): TestSymlink{Target: filepath.FromSlash("../../foo")},
@@ -366,7 +365,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
},
want: TestDir{
@@ -377,7 +376,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
"bar": TestFile{Content: "bar"},
},
@@ -389,7 +388,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
},
want: TestDir{
@@ -401,7 +400,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
},
want: TestDir{
@@ -414,7 +413,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
},
want: TestDir{
@@ -425,7 +424,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
},
want: TestDir{
@@ -436,7 +435,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
},
{
expectFailure: true,
files: map[string]interface{}{
files: map[string]any{
"foo": TestFile{Content: "foo"},
},
want: TestDir{
@@ -449,8 +448,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
for _, test := range tests {
t.Run("", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
tempdir := rtest.TempDir(t)
@@ -467,7 +465,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
repo := repository.TestRepository(t)
arch := New(repo, fs.Local{}, Options{})
arch := New(repo, fs.NewLocal(), Options{})
opts := SnapshotOptions{
Time: time.Now(),
Hostname: "localhost",
+3 -3
View File
@@ -274,16 +274,16 @@ func unrollTree(f fs.FS, t *tree) error {
return nil
}
// BackupTarget is a resolved backup path and whether the user passed this path
// backupTarget is a resolved backup path and whether the user passed this path
// literally. Paths inserted when expanding a target with no path components
// (for example ".") have Explicit false so include/exclude rules still apply.
type BackupTarget struct {
type backupTarget struct {
Path string
Explicit bool
}
// newTree creates a Tree from the target files/directories.
func newTree(fs fs.FS, targets []BackupTarget) (*tree, error) {
func newTree(fs fs.FS, targets []backupTarget) (*tree, error) {
debug.Log("targets: %v", targets)
tree := &tree{}
seen := make(map[string]struct{})

Some files were not shown because too many files have changed in this diff Show More