Compare commits

..
682 Commits
Author SHA1 Message Date
Alexander Neumann 6335213501 Set development version for 0.19.1 2026-07-05 09:52:54 +02:00
Alexander Neumann 6aa3a516ce Add version for 0.19.1 2026-07-05 09:52:50 +02:00
Alexander Neumann b1da29738c Generate CHANGELOG.md for 0.19.1 2026-07-05 09:52:30 +02:00
Alexander Neumann 09a1eb6714 Prepare changelog for 0.19.1 2026-07-05 09:52:30 +02:00
Michael Eischer c2b4d6a959 Patch release cherry-pick (#21914) 2026-06-24 20:06:20 +02:00
Michael Eischer 437945bf90 Merge pull request #21911 from restic/patch-release-polish-changelogs
Polish changelogs
2026-06-24 19:48:35 +02:00
Michael Eischerandrawtaz fc422d4145 Apply suggestions from code review
Co-authored-by: rawtaz <rawtaz@users.noreply.github.com>
2026-06-24 19:30:11 +02:00
Michael Eischer f058670219 polish changelogs 2026-06-23 19:55:12 +02:00
Michael Eischer df8454770b Merge pull request #21910 from restic/patch-release-picks
Cherry pick changes for patch release
2026-06-23 19:54:25 +02:00
Michael Eischer c9185dd19c repository/index: ensure progress bar cancelation on early index load failure (#21881) 2026-06-23 19:40:50 +02:00
Ricardo Sawir 32be2e559b 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-23 19:26:29 +02:00
Eyüp Can Akman add4fa1efb backup: skip source paths that cannot be accessed (#21852) 2026-06-23 19:26:13 +02:00
Michael Eischer c7c3414641 archiver: ignore duplicate but excluded directory entry (#21900) 2026-06-23 19:26:10 +02:00
Michael Eischer 8ef295e2f5 snapshots: revert default --lastest behavior to pre-0.19.0
the changed behavior now only applies when using `--group-by`.
2026-06-23 19:15:14 +02:00
Michael Eischer bc714d1864 snapshots: cleanup handling of deprecated --last option 2026-06-23 19:15:14 +02:00
Michael Eischer debf95f58d backend/sftp: fix removing files on windows sftp 2026-06-23 19:14:54 +02:00
Michael Eischer 27c44c11aa backend/sftp: fix error reporting of Save method 2026-06-23 19:14:54 +02:00
Michael Eischer 93f695a803 backend/rest: fix flaky test cleanup (#21901) 2026-06-23 19:14:54 +02:00
Michael Eischer 3e21d1fea6 fs: print missing error if xattr cannot be read (#21896) 2026-06-23 19:13:39 +02:00
Michael Eischer 8ee674da55 repository: show correct error if zstd decoder cannot be initialized 2026-06-23 19:13:39 +02:00
Michael Eischer c21f0fabc6 selfupdate: improve error handling 2026-06-23 19:13:39 +02:00
Michael Eischer 6aaa2eace4 prune/repair pack: document ignored errors 2026-06-23 19:13:39 +02:00
Michael Eischer f2d614db56 backend/util: fix errorhandling in DefaultDelete test helper 2026-06-23 19:13:39 +02:00
Michael Eischer ed655b476a backend/sftp: fix file handle leak if chmod for a file fails 2026-06-23 19:13:39 +02:00
Michael Eischer 80f694b32e backend/rest: correctly close body on out of bounds read 2026-06-23 19:13:39 +02:00
Michael Eischer eaa835becd backend/b2: correctly close file if too short 2026-06-23 19:13:39 +02:00
Michael Eischer 7aa82d68b4 forget: fix error handling if snapshot key cannot be marshaled 2026-06-23 19:13:39 +02:00
Michael Eischer a3fa3eb182 ensure reliable cleanup of test repository (#21880) 2026-06-23 19:12:08 +02:00
Michael Eischer cb24c4f566 snapshots: show location name instead of current timezone (#21877) 2026-06-23 19:11:19 +02:00
Michael Eischer d6fd4ae26f mount: fix crash in mountpoint validation (#21879) 2026-06-23 19:09:57 +02:00
Michael Eischer 4134bdfac2 stats: hide progress bar for json output (#21871) 2026-06-23 19:08:36 +02:00
Michael Eischer c08e7fd956 mount: unify mountpoint validation error messages 2026-06-23 19:08:36 +02:00
Michael Eischer d07bcd50da mount: factor out mountpoint validation into helper 2026-06-23 19:08:36 +02:00
Srigovind Nayak e6b9118f51 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-06-23 19:08:12 +02:00
Srigovind Nayak 82a665384b chore: update changelog for issue 5234 2026-06-23 19:08:12 +02:00
Srigovind Nayak 1e249b0da1 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-06-23 19:08:12 +02:00
Michael Eischer 5967f9a894 upgrade compress library 2026-06-23 19:07:56 +02:00
Michael Eischer 1733dd56b9 repository: fix zero-sized blobs in v2 repos 2026-06-23 19:07:56 +02:00
Michael Eischer ed39b60e7d fix version set by helper after release (#21865) 2026-06-23 19:06:55 +02:00
Alexander Neumann 12875cc48e Add version for 0.19.0 2026-06-09 18:49:04 +02:00
Alexander Neumann 8c37a3983a Update manpages and auto-completion 2026-06-09 18:49:04 +02:00
Alexander Neumann d718c4cf20 Generate CHANGELOG.md for 0.19.0 2026-06-09 18:48:35 +02:00
Alexander Neumann 60448342b3 Prepare changelog for 0.19.0 2026-06-09 18:48:35 +02:00
Alexander Neumann 4f59ed8806 Merge pull request #21863 from MichaelEischer/downgrade-compress
Downgrade compress library due to regression with zero-sized blobs
2026-06-09 18:40:30 +02:00
Michael Eischer 522efd1175 downgrade compress library due to regression with zero-sized blobs
while this should only affect test cases better be safe than sorry.
2026-06-08 22:51:47 +02:00
rawtaz f01359ff92 doc: Polish changelogs (#21847) 2026-06-05 22:30:46 +02:00
rawtaz 0b3c0fc222 Merge pull request #21842 from restic/polish-changelogs
Polish changelogs
2026-06-05 00:33:38 +02:00
Winfried Plappert ed571c8d12 Docs - Diving into a Repository - create html links across the chapter (#21839) 2026-06-02 22:42:43 +02:00
Michael Eischer 0ddd98e03b stats: stop progress bar before printing stats (#21843) 2026-06-02 22:37:46 +02:00
Michael Eischer cebc5c8957 polish changelogs 2026-06-01 22:10:27 +02:00
dependabot[bot] 724b7dc231 build(deps): bump google.golang.org/api from 0.279.0 to 0.282.0 (#21836)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 19:39:49 +00:00
Michael Eischer 2141fd83a9 checker: increase timeout for blob type confusion test (#21831) 2026-06-01 21:29:52 +02:00
dependabot[bot] 6693611497 build(deps): bump github.com/minio/minio-go/v7 from 7.1.0 to 7.2.0 (#21838)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 21:26:05 +02:00
dependabot[bot] 0e9446277d build(deps): bump cloud.google.com/go/storage from 1.62.1 to 1.62.2 (#21837)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 21:25:14 +02:00
dependabot[bot] b6bfa13f5f build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob from 1.6.4 to 1.7.0 (#21835)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 21:24:40 +02:00
dependabot[bot] 88aa8132ae build(deps): bump actions/checkout from 4 to 6 (#21834)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 21:23:28 +02:00
dependabot[bot] bff8acb2d9 build(deps): bump docker/login-action from 4.0.0 to 4.2.0 (#21833)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 21:22:35 +02:00
dependabot[bot] 4ae2e26021 build(deps): bump the golang-x-deps group with 3 updates (#21832)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 21:22:25 +02:00
MetalSporkandMichael Eischer 042feb2b7f docs: expand restic cat documentation (#5679)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-05-31 15:46:45 +00:00
Michael Eischer 474c094cac Merge pull request #21817 from eyupcanakman/fix/sftp-dir-permissions
sftp: Use mode 0700 for repository directories
2026-05-31 16:40:17 +02:00
Winfried PlappertandMichael Eischer 7566069bf3 docs: amend restic find documentation (#21818)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-05-31 16:31:44 +02:00
Michael Eischer 53de77ea1a Merge pull request #21828 from MichaelEischer/fix-split-pack-index
repository: repair index: correctly handle split index entries
2026-05-31 16:30:49 +02:00
Michael Eischer a54ac48f60 restic: test that sorting nil blobs slices works 2026-05-31 16:22:58 +02:00
Michael Eischer 91a6e74fd5 repository: test split index handling in index rewrite 2026-05-31 16:22:58 +02:00
Michael Eischer e666980cec add changelog for split pack index handling 2026-05-31 16:22:58 +02:00
Michael Eischer 78b3411076 check: consider split pack index entries as repository damage 2026-05-31 15:58:33 +02:00
Michael Eischer 5b39ad861e repository: repair index: correctly handle split index entries
In restic <0.10.0, it was possible that the blobs of a pack file were
split across multiple indexes. `MasterIndex.Rewrite` however assumed
that each an index always contains the full description of a pack file.
Therefore, further index entries for a pack were filtered out as
duplicates. Now, the code also checks the blobs contained in the index
entry while filtering out duplicates.
2026-05-31 15:58:29 +02:00
Michael Eischer f86307d223 Merge pull request #21827 from MichaelEischer/fix-pack-repair
repair packs: correctly handle packs with missing/incomplete index entry
2026-05-31 15:57:50 +02:00
Michael Eischer 77a6bf3bb7 Merge pull request #21797 from MichaelEischer/always-include-explicit-targets
backup: prevent exclude of backup targets
2026-05-31 15:42:26 +02:00
Michael Eischer c95ef18afb repository: fix error handling in repair pack if blob upload fails 2026-05-31 15:40:15 +02:00
Michael Eischer 4c23c5946f add repair packs changelog 2026-05-31 15:40:15 +02:00
Michael Eischer 640b2489f6 repository: repair pack: test handling of not indexed packs 2026-05-31 15:40:15 +02:00
Michael Eischer 6a3f447327 repository: repair pack: correctly handle incomplete index 2026-05-31 15:40:15 +02:00
Michael Eischer ce24640d75 backup: prevent hang using --stdin-from-command if upload fails (#21829) 2026-05-31 15:27:05 +02:00
Michael Eischer 7e8f9a9221 document excludes for explicit backup sources 2026-05-31 15:24:43 +02:00
Eyüp Can Akman 62cf574fd8 sftp: Use mode 0700 for repository directories
The SFTP backend created repository directories with pkg/sftp's Mkdir
and MkdirAll, which take no mode argument, so the directories inherited
the SFTP server's umask instead of the 0700 used for local
repositories. Set the mode of each directory the backend creates.
2026-05-31 12:40:26 +03:00
Michael Eischer 32bcd92f60 archiver: test that explicit backup paths ignore excludes 2026-05-30 22:30:30 +02:00
Michael Eischer 02cf8e5f23 backup: prevent exclude of backup targets
Track backup targets explicitly specified by the user and prevent
excluding them. This for example ensures that `restic backup
--exclude-if-present .git /home/user/data` backs up the `data` folder
even if there is a `.git` folder in `/home/user`.

Note that this does not suffice for commands like `restic backup --exclude data /home/user/data`
as the exclude pattern will still match every single file within `data`.
2026-05-30 22:30:30 +02:00
Yaroslav HalchenkoandClaude Code 2.1.142 / Claude Opus 4.7 451cc6c048 Add codespell support with configuration and fixes (#21807)
Co-authored-by: Claude Code 2.1.142 / Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 20:15:13 +00:00
Michael Eischer c221cd06ad repository: forget id of unreadable packs on index rebuild (#21826) 2026-05-30 22:09:33 +02:00
Michael Eischer e7d3a4ae51 repository: repair pack: move pack reupload to helper 2026-05-30 10:15:16 +02:00
Michael Eischer 2a95591b48 index: replace custom EachByPackResult datatype 2026-05-30 10:11:18 +02:00
Michael Eischer c669cc7a7d introduce restic.Blobs type with sort method 2026-05-30 10:10:39 +02:00
Michael Eischer 2802bec8b9 Polish changelogs (#21805) 2026-05-29 21:06:29 +02:00
Michael Eischer f000da3b35 Return helpful error if subfolder syntax fails on Windows (#21813) 2026-05-20 22:55:01 +02:00
Michael Eischer dd4f1f06a1 Merge pull request #21810 from MichaelEischer/cmd-doc-typos
Fix command doc typos and update help for check/rewrite
2026-05-20 22:54:40 +02:00
Michael Eischer 77f6795b32 doc: update embedded help output 2026-05-20 22:46:11 +02:00
Michael Eischer 850ab61cdf key: include full command name in subcommand descriptions 2026-05-20 22:46:11 +02:00
Michael Eischer df03e8a0ec self update: remove not possible exit codes 2026-05-20 22:46:11 +02:00
Michael Eischer 9299c45818 fix typos in cmd descriptions 2026-05-20 22:46:11 +02:00
Michael Eischer 2b72f3eb42 find: fix placement of exit status section 2026-05-20 22:46:11 +02:00
Michael Eischer 496ea804a6 check/dump/mount/repair/rewrite: update command description 2026-05-20 22:45:52 +02:00
Michael Eischer ccfb31b5fa filter: correctly stop pattern validation on first invalid part (#21812) 2026-05-20 22:42:26 +02:00
Michael Eischer a639b8d711 Merge pull request #21811 from MichaelEischer/misc-fixes
Address various code smells, outdated comments and nits
2026-05-20 22:38:36 +02:00
Michael Eischer de55280003 Merge pull request #21809 from MichaelEischer/doc-typos
doc: fix typos, unify section heading style and minor additions
2026-05-20 22:32:59 +02:00
Winfried PlappertandMichael Eischer 990329013e prune more aggresively (#21803)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-05-16 15:49:08 +00:00
Michael Eischer 10645ccd2a fix comment and variable name typos 2026-05-16 17:05:33 +02:00
Michael Eischer fa1a318780 fs: drop outdated comment regarding UNC paths on Windows 2026-05-16 17:05:33 +02:00
Michael Eischer 8b6eff5a47 filter: fix comment for validatePatterns 2026-05-16 17:05:33 +02:00
Michael Eischer 770ec1e25a doc: clarify ** usage 2026-05-16 17:01:52 +02:00
Michael Eischer febdb8b564 find: use proper sentinel error to terminate search 2026-05-16 15:35:48 +02:00
Michael Eischer 91c2aefbe1 Document openWithReadLock usage of read-only backend mode 2026-05-16 15:35:48 +02:00
Michael Eischer 3cc592463f Don't lower case case-insensitive patterns in place 2026-05-16 15:35:42 +02:00
Michael Eischer c04a1d857d backend/retry: debug log correct error on failed file removal 2026-05-16 15:35:42 +02:00
Michael Eischer bd945df2ea archiver: note that fileSaver.Save expects non-blocking callbacks 2026-05-16 15:35:42 +02:00
Michael Eischer 5105015f5d check: filter packs while holding blobRefs lock
Not an actual problem with the current usage. But for consistency always
take the lock when interacting with blobRefs.
2026-05-16 15:35:42 +02:00
Michael Eischer d4397926cc restorer: drop redundant nil check 2026-05-16 15:35:42 +02:00
Michael Eischer 5caa33e7b9 repository/pack: prevent packer usage after error
In-depth hardening to prevent packer reuse after an error.
2026-05-16 15:35:42 +02:00
Michael Eischer ef750c4c5d archiver: reuse buffer if reading from file failed 2026-05-16 15:35:42 +02:00
Michael Eischer 3148494a92 archiver: check chunker error before updating the node
This is actually just a cosmetic issue as chunk.Length is 0 if the
chunker returned an error.
2026-05-16 12:03:43 +02:00
Michael Eischer e212076f71 doc: correctly use RST backticks
RST uses double backticks for inline code unlike the single backticks in
Markdown.
2026-05-15 23:37:54 +02:00
Michael Eischer b16f0bf410 doc: use sentence case for section headings 2026-05-15 23:37:54 +02:00
Michael Eischer 75f92f22d9 doc: unify section headings to gerund phrases
Use -ing headings for sections across the user manual.
2026-05-15 23:37:54 +02:00
Michael Eischer 4c9a07e9c8 doc: stick to second-persion "you" in user docs
Replace usages of `we`, `one` and "it is possible" with the mainly used `you`.
2026-05-15 23:13:29 +02:00
Michael Eischer 5bc7398e5e doc: sync S3 path in tutorial with S3 path in reference
Regional endpoints should be preferred. Omitting `https://` defaults to
https.
2026-05-15 22:55:22 +02:00
Michael Eischer 07bb05deaa doc: add RESTIC_FROM_* environment variables to scripting section 2026-05-15 22:45:08 +02:00
Michael Eischer 4b52ef9a97 doc: correct description of forget --tag 2026-05-15 22:44:27 +02:00
Michael Eischer bd7f3483ac doc: clarify repository setup for copy 2026-05-15 22:43:09 +02:00
Michael Eischer f6c8751575 doc: consistency fixes 2026-05-15 22:42:29 +02:00
Michael Eischer 2419c03177 doc: fix several typos 2026-05-15 22:25:51 +02:00
Michael Eischer b7d8c98214 docs: clarify backup step in troubleshooting guide (#21802) 2026-05-15 21:23:48 +02:00
Michael Eischer 265d070255 ui: Fix data race and minor API cleanup (#21801) 2026-05-15 21:23:24 +02:00
Paulo SaraivaandPaulo Saraiva 6de6cc6c75 Show progress for restic stats (#5705)
Co-authored-by: Paulo Saraiva <paulo.saraiva@cern.ch>
2026-05-15 20:31:20 +02:00
Michael Eischer 8abbc3703d doc: update embedded help output and misc typos (#21804) 2026-05-15 20:21:04 +02:00
7acab7b1a9 add example for "Pulling a Backup with HTTP over a ssh tunnel" (#5504)
Co-authored-by: Leo R. Lundgren <leo@finalresort.org>
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-05-14 12:52:33 +00:00
Michael Eischer 5d70bf9f10 Merge pull request #5643 from kjetilho/doc-restore
Improve documentation for restore
2026-05-14 14:51:45 +02:00
Michael Eischer 6c63c1c244 partially modernize restore command output 2026-05-14 13:43:54 +02:00
Michael Eischer acc95531a4 docs/restore: clarify --path option example, subfolder syntax and --delete option 2026-05-14 13:42:33 +02:00
Michael Eischer bf14a94600 Merge pull request #21784 from jtru/fuse-mount-hardlink-count
mount: Ensure a hard link count > 0 for all files
2026-05-14 11:25:20 +02:00
Michael Eischer 4547fd7b18 fuse: tweak comment 2026-05-14 11:18:16 +02:00
Michael Eischer 65d90641bb add changelog for windows hardlink count fix 2026-05-14 11:18:16 +02:00
Michael Eischer 7878485eec Merge pull request #5648 from dgkmd/feature/statusbar
ui: don't rewrite unchanged lines in status bar
2026-05-14 10:50:01 +02:00
Michael Eischer d494e37dc1 ui/termstatus: reorder findUnchangedLines function 2026-05-14 10:42:13 +02:00
Michael Eischer 59697213f9 ui/termstatus: cleanup test code 2026-05-14 10:42:13 +02:00
Michael Eischer df2d65bb88 ui/termstatus: test skipping of unchanged lines 2026-05-14 10:42:13 +02:00
Michael Eischer bd8aad3b9b ui/termstatus: deduplicate error handling 2026-05-14 10:42:13 +02:00
Michael Eischer cf34130a05 ui/termstatus: simplify status tracking 2026-05-14 10:42:13 +02:00
Donggyu Kim e33bcede2f terminal: Do not write unchanged status lines
Check if each line of status is changed, and write
the line to the terminal only if it has changed
2026-05-14 10:42:13 +02:00
Michael Eischer 4f781b69f9 add hardlink test 2026-05-13 22:29:34 +02:00
dependabot[bot] ff575a978d build(deps): bump google.golang.org/api from 0.278.0 to 0.279.0 (#5734)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 20:27:31 +00:00
Adrian Mastronardi 9a0432a5d7 docs: clarify double-asterisk wildcard behavior in exclude patterns (#5726) 2026-05-13 20:25:36 +00:00
Michael Eischer f3854cf299 Merge pull request #21796 from restic/go-1.25
Bump minimum go version to 1.25 & update dependencies
2026-05-12 18:56:09 +02:00
Michael Eischer f3ca1a5c13 Merge pull request #21798 from MichaelEischer/fix-windows-test-flake
windows: try to fix flaky tests
2026-05-12 18:53:31 +02:00
Michael Eischer a213daef29 windows: improve randomness of temp file name
Creating two temporary files at nearly the same time could result in a
filename collision.
2026-05-11 22:50:48 +02:00
Michael Eischer e3f065ad54 windows: ignore temporary access denied error on temp file creation 2026-05-11 22:42:51 +02:00
Michael Eischer 7ee77133fc Merge pull request #21781 from mattxtaz2/master 2026-05-10 22:20:24 +02:00
Michael Eischer 1cdb0a9c96 add changelog for key passwd fix 2026-05-10 22:13:16 +02:00
Michael Eischer f8afd4ed04 key passwd: test that user and host flags are handled 2026-05-10 22:13:16 +02:00
Michael Eischer 4a63f7fd13 Merge pull request #5701 from jylenhof/master 2026-05-10 21:31:15 +02:00
Michael Eischer 10475c9841 update all direct dependencies 2026-05-10 18:07:18 +02:00
Michael Eischer 5608e86cdb Merge pull request #5699 from MichaelEischer/dump-with-relative-paths 2026-05-10 17:54:08 +02:00
Michael Eischer bdb2196378 Bump golangci version 2026-05-10 17:53:29 +02:00
Michael Eischer 4c94678d7d fix linter and compilation issues 2026-05-10 17:53:29 +02:00
Michael Eischer a241652787 windows: fix hang while reading from directory 2026-05-10 17:53:29 +02:00
Michael Eischer a1c870c675 Simplify smoke test debugging 2026-05-10 17:53:08 +02:00
Michael Eischer 6080c784e4 Bump minimum go version to 1.25 2026-05-10 17:53:08 +02:00
Michael Eischer ed418d8d60 document that switching between relative and absolute paths prevents change detection 2026-05-10 17:40:55 +02:00
Michael Eischer 93357440da doc: describe effects of relative paths on snapshot structure 2026-05-10 17:34:42 +02:00
Michael Eischer 5b7df8135e Merge pull request #5713 from MichaelEischer/index-optimization 2026-05-10 01:09:06 +02:00
Michael Eischer adce279d04 add changelog 2026-05-10 00:38:01 +02:00
Michael Eischer 5c935e71fa index: also preallocate hashed array tree 2026-05-10 00:35:17 +02:00
Michael Eischer 934c615e51 index: support index preallocation 2026-05-10 00:35:17 +02:00
Michael Eischer ba638b6602 indexmap: use bloom filter to drastically speed up check for unknown blobs
Only in use on 64-bit systems. Use the upper 28bits of the id of an
index entry as bloom filter. This allows skipping the index entry
traversal most of the time if an id is not stored in the hashmap.

The bloom filter embedded in the index entry id is check each time
before following a reference to an index entry. This further reduces
the risk of false positives. The bloom filter itself is basically for
free on modern CPUs.

The main performance cost of checking for unknown blobs in the index are
the essentially random RAM accesses for the initial bucket lookup as
well as following the next pointer in the index entries. With the bloom
filter most of the time only the initial bucket lookup is necessary.

This speeds up checking for unknown blobs by a factor 5 (!), while
having no effect on the lookup of known blobs:

$ benchstat no-bloom with-bloom
name                old time/op  new time/op  delta
IndexHasUnknown-16  49.0ms ± 2%   9.9ms ± 7%  -79.70%  (p=0.000 n=10+10)
IndexHasKnown-16    48.0ms ± 3%  47.9ms ± 3%     ~     (p=0.968 n=10+9)

This bloom filter parameters m=28 k=1 were derived empirically, while
also leaving sufficient room for very large repositories. Before this
commit, the final merge index step took roughly 1 second per million
index entries. With the chosen bloom filter parameters, it would
currently take 19 hours to just merge such an index. It is safe to
assume that such large repositories don't exist.

Comparison with other parameter sets:

$ m=28 k=1 versus m=32 k=1
name                old time/op  new time/op  delta
IndexHasUnknown-16  49.0ms ± 2%   9.7ms ±16%  -80.17%  (p=0.000 n=10+10)
IndexHasKnown-16    48.0ms ± 3%  48.4ms ± 3%     ~     (p=0.436 n=10+10)

$ m=28 k=1 versus m=24 k=1
name                old time/op  new time/op  delta
IndexHasUnknown-16  49.0ms ± 2%  10.8ms ±13%  -77.90%  (p=0.000 n=10+10)
IndexHasKnown-16    48.0ms ± 3%  47.9ms ± 3%     ~     (p=0.684 n=10+10)

$ m=28 k=1 versus m=28 k=2
name                old time/op  new time/op  delta
IndexHasUnknown-16  49.0ms ± 2%  24.9ms ± 5%  -49.27%  (p=0.000 n=10+10)
IndexHasKnown-16    48.0ms ± 3%  48.0ms ± 4%     ~     (p=1.000 n=10+10)

`k=2` outright wrecks the performance. This is most likely the case as
it performs worse on longer index entry chains, which also happen to be
the expensive ones to process.

`m=32` yields diminishing returns, while getting within an order of
magnitude of the largest known restic repositories.

Design alternatives:

In principle it would be possible to add a single large bloom filter
instead of embedding them in the index entry ids. However, this bloom
filter would necessarily incur additional random memory accesses and
thus slow things down overall.
2026-05-10 00:35:17 +02:00
Michael Eischer 320f709fbc index: modernize masterindex tests
`b.Loop()` drastically shortens benchmark execution times for tests with
an expensive initialization phase as it only has to happen once now.
2026-05-10 00:35:17 +02:00
Michael Eischer e33ed5d0c1 index: make tests more representative 2026-05-10 00:35:17 +02:00
Michael Eischer 39084a912e Merge pull request #5700 from MichaelEischer/err-invalid-env 2026-05-10 00:18:40 +02:00
jylenhof 24c3b73f7c doc(installation): add mise alternative installation method
Signed-off-by: jylenhof <jygithub@lenhof.eu.org>
2026-05-08 12:20:18 +02:00
Michael Eischer f5ab39b33d mount: avoid duplicate index loading (#5720) 2026-05-07 22:59:55 +02:00
Michael Eischer ca555897f2 Add incremental index loading changelog 2026-05-07 22:52:23 +02:00
Michael Eischer 4c0dc9e202 index: support incremental index loading
Do not require a full index reload if only a few additional index files
have been added. This can drastically speed up loading the index in the
mount command.
2026-05-07 22:52:03 +02:00
Michael Eischer b24d210b45 mount: load mount root before reporting readiness 2026-05-07 22:42:03 +02:00
dependabot[bot] 3dcd60d979 build(deps): bump docker/metadata-action from 5 to 6 (#21764)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 22:31:56 +02:00
dependabot[bot] fbf6fad74f build(deps): bump github.com/klauspost/compress from 1.18.3 to 1.18.4 (#5733)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 22:31:34 +02:00
dependabot[bot] 302e713c5d build(deps): bump github.com/elithrar/simple-scrypt from 1.4.0 to 1.4.1 (#5736)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 22:29:50 +02:00
dependabot[bot] 2ef86a8410 build(deps): bump docker/setup-buildx-action from 3 to 4 (#21763)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 22:16:29 +02:00
dependabot[bot] 760610a29a build(deps): bump docker/build-push-action from 6 to 7 (#21765)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 22:16:22 +02:00
dependabot[bot] 48d7f232c2 build(deps): bump docker/setup-qemu-action from 3 to 4 (#21766)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 22:15:32 +02:00
dependabot[bot] 25e72d3259 build(deps): bump docker/login-action from 3.7.0 to 4.0.0 (#21768)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 22:15:23 +02:00
Rohan Santhosh KumarandCodex 90ce78dbaa docs: fix Nix installation typo (#21772)
Co-authored-by: Codex <codex@openai.com>
2026-05-07 22:13:20 +02:00
Johannes Truschnigg 461bddb0e8 mount: Ensure a hard link count > 0 for all files
When using `restic mount` to serve a repository via a POSIX host's file
system, all files backed up from Windows systems will show up on the
mounting host with a (hard)link count of 0.

While this is not a problem in general and most programs do not even
register this strange value, some others (such as Samba's smbd(8)) will
go the extra mile and check a file's stat() results for various
properties (such as a positive non-zero link count), and refuse to
operate if any of the reported values appear off.

Since other inode properties absent from non-POSIX backup sources are
also "faked up" (e.g., the inode number) during mount and work fine in
general, the chances of this change to be in any way harmful are
probably rather slim.
2026-04-19 13:44:18 +02:00
Matt 0f8c28aee4 Add opts.Username, opts.Hostname to AddKey in the "key passwd" command
so that --user and --host flags work
2026-04-16 19:15:44 +01:00
rawtaz 1807d269cd Merge pull request #21769 from Rohan5commit/fix-participating-doc-typo-20260401
Fix a typo in the contributing docs
2026-04-01 12:19:59 +02:00
Codex b1e9811ffc Fix a typo in the contributing docs 2026-04-01 10:32:50 +08:00
Winfried Plappert 54347eb6fa Enhancement: restic find - more integration tests (#5672) 2026-02-26 21:52:26 +01:00
Michael Eischer 271d622823 Merge pull request #5725 from AdrianMastronardi/fix/4258-document-backup-progress-bar 2026-02-26 21:29:29 +01:00
Michael Eischer 541eb5cfd8 clarify changelog expectations (#5721) 2026-02-25 20:36:07 +00:00
Winfried Plappert f9b63050eb Bugfix: `restic find --pack <tree-pack>` did not produce output for tree packs (#5664) 2026-02-25 21:24:31 +01:00
rawtaz 49c988be07 Merge pull request #5724 from AdrianMastronardi/fix/5218-dead-link-installation-docs
docs: remove outdated CentOS 6/7 installation instructions
2026-02-24 12:08:32 +01:00
Adrian Mastronardi 9b1d45935a docs: remove version-specific references from RHEL/CentOS instructions 2026-02-24 01:02:42 +01:00
Adrian Mastronardi 102ea6da2b docs: document backup progress bar fields (#4258) 2026-02-24 00:06:38 +01:00
Adrian Mastronardi 4e71921a17 docs: remove outdated CentOS 6/7 installation instructions (#5218) 2026-02-23 23:15:49 +01:00
Winfried Plappert c5e09ae9b1 docs: expand restic find - documentation (#5675) 2026-02-19 18:07:05 +00:00
Winfried Plappert 1f329cd933 docs: expand documentation about testing (#5346) 2026-02-19 18:26:15 +01:00
Johannes TruschniggandMichael Eischer a8f0ad5cc4 mount: check for more requisite mountpoint conditions (#5718)
* mount: check for more requisite mountpoint conditions

In order to be able to mount a repository over a mountpoint target
directory via FUSE, that target directory needs to be both writeable and
executable for the UID performing the mount.

Without this patch, `restic mount` only checks for the target pathname's
existence, which can lead to a lot of data transfer and/or computation
for large repos to be performed before eventually croaking with a fatal
"fusermount: failed to chdir to mountpoint: Permission denied" (or
similar) error.

FUSE does allow for mounting over a target path that refers to a regular
(writeable) file, but the result is not accessible via chdir(), so we
prevent that as well, and accept only directory inodes as the intended
target mountpoint path.

* Don't use snake_case identifiers

* Add changelog entry

* tweak changelog summary

---------

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-02-19 17:11:49 +00:00
Michael Eischerandudf2457 4c56384481 docs: describe assigning ambient capabilities using systemd (#5698)
---

Co-authored-by: udf2457 <udf2457@users.noreply.github.com>
2026-02-18 22:33:21 +01:00
Winfried Plappert 8b567a9270 Bugfix restic find: missing check for mtime --oldest/--newest (#5310) 2026-02-18 21:14:35 +00:00
Michael Eischer 27c560b371 Merge pull request #5650 from fabien-joubert/docs-warning-capabilities
docs: add warning for capability-based non-root backups
2026-02-18 21:44:04 +01:00
Andreas Scherbaum 66d915ef79 Add space in error message (#5704) 2026-02-18 20:27:02 +00:00
Michael EischerandIlya Grigoriev 7077500a3b Have backup -vv mention compressed size of added files (#5669)
ui: mention compressed size of added files in `backup -vv`

This is already shown for modified files, but the added files message
wasn't updated when compression was implemented in restic.

Co-authored-by: Ilya Grigoriev <ilyagr@users.noreply.github.com>
2026-02-18 21:24:29 +01:00
Michael Eischer 6566f786e9 stats: also print snapshot size statistics in debug mode (#5712) 2026-02-18 21:21:40 +01:00
Michael Eischer d1937a530b clarify pack ID in decryption error (#5710)
pack ID is included in full. In addition, the error message now says
that it is a pack file.
2026-02-18 20:43:10 +01:00
Michael Eischer 5be6d9c73f fail of RESTIC_READ_CONCURRENCY or RESTIC_COMPRESSION are invalid 2026-02-01 15:57:07 +01:00
gunarandMichael Eischer 7101f11133 Fail fast for invalid RESTIC_PACK_SIZE env values (#5592)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-02-01 15:45:31 +01:00
Michael Eischer 8bff5cead0 Merge pull request #5696 from restic/dependabot/go_modules/github.com/minio/minio-go/v7-7.0.98 2026-02-01 12:29:55 +01:00
Michael Eischer 5e43a44b15 Merge pull request #5680 from castilma/unlock-doc 2026-02-01 12:13:52 +01:00
Michael Eischer 67c13c643d Merge pull request #5691 from MichaelEischer/fix-rewriter-error 2026-02-01 12:09:52 +01:00
dependabot[bot] b706c19614 build(deps): bump github.com/minio/minio-go/v7 from 7.0.97 to 7.0.98
Bumps [github.com/minio/minio-go/v7](https://github.com/minio/minio-go) from 7.0.97 to 7.0.98.
- [Release notes](https://github.com/minio/minio-go/releases)
- [Commits](https://github.com/minio/minio-go/compare/v7.0.97...v7.0.98)

---
updated-dependencies:
- dependency-name: github.com/minio/minio-go/v7
  dependency-version: 7.0.98
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 11:04:31 +00:00
Michael Eischer da2ed89ffd Merge pull request #5697 from restic/dependabot/go_modules/github.com/klauspost/compress-1.18.3 2026-02-01 12:01:36 +01:00
Michael Eischer cf3793bb41 Merge pull request #5695 from restic/dependabot/go_modules/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob-1.6.4 2026-02-01 12:00:39 +01:00
Michael Eischer db8e379fd4 Merge pull request #5694 from restic/dependabot/go_modules/cloud.google.com/go/storage-1.59.2 2026-02-01 12:00:05 +01:00
Michael Eischer 4f73daa761 Merge pull request #5693 from restic/dependabot/go_modules/golang-x-deps-173d0ad829 2026-02-01 11:59:31 +01:00
Michael Eischer 48cfa908ed Merge pull request #5692 from restic/dependabot/github_actions/docker/login-action-3.7.0 2026-02-01 11:56:38 +01:00
Michael Eischer d3c225627f Merge pull request #5682 from wplapper/docs_list 2026-02-01 11:55:37 +01:00
Michael Eischer 07d380d54b Merge pull request #5191 from wplapper/cmd_rewrite_include 2026-02-01 11:53:05 +01:00
Winfried Plappert b544e71cac restic list doc - documenmtation
fixed wording for paragraph and inconsistent underlining
2026-02-01 06:12:56 +00:00
Winfried Plappert 099650f883 docs: restic list
corrected typo
2026-02-01 06:02:39 +00:00
Winfried Plappert 6154685c3a docs: add documentation for restic list
added a file doc/view_repository.rst which contains the
description of `restic list ...`
2026-02-01 06:02:39 +00:00
dependabot[bot] 66bb196591 build(deps): bump github.com/klauspost/compress from 1.18.2 to 1.18.3
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.2 to 1.18.3.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.2...v1.18.3)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 01:43:15 +00:00
dependabot[bot] 2be17d2313 build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
Bumps [github.com/Azure/azure-sdk-for-go/sdk/storage/azblob](https://github.com/Azure/azure-sdk-for-go) from 1.6.3 to 1.6.4.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/storage/azblob/v1.6.3...sdk/storage/azblob/v1.6.4)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
  dependency-version: 1.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 01:42:57 +00:00
dependabot[bot] 34ba097162 build(deps): bump cloud.google.com/go/storage from 1.58.0 to 1.59.2
Bumps [cloud.google.com/go/storage](https://github.com/googleapis/google-cloud-go) from 1.58.0 to 1.59.2.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/spanner/v1.58.0...storage/v1.59.2)

---
updated-dependencies:
- dependency-name: cloud.google.com/go/storage
  dependency-version: 1.59.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 01:42:48 +00:00
dependabot[bot] 38f1fb61f3 build(deps): bump the golang-x-deps group with 5 updates
Bumps the golang-x-deps group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.46.0` | `0.47.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.48.0` | `0.49.0` |
| [golang.org/x/sys](https://github.com/golang/sys) | `0.39.0` | `0.40.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.38.0` | `0.39.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.32.0` | `0.33.0` |


Updates `golang.org/x/crypto` from 0.46.0 to 0.47.0
- [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0)

Updates `golang.org/x/net` from 0.48.0 to 0.49.0
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

Updates `golang.org/x/sys` from 0.39.0 to 0.40.0
- [Commits](https://github.com/golang/sys/compare/v0.39.0...v0.40.0)

Updates `golang.org/x/term` from 0.38.0 to 0.39.0
- [Commits](https://github.com/golang/term/compare/v0.38.0...v0.39.0)

Updates `golang.org/x/text` from 0.32.0 to 0.33.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/sys
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/term
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/text
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 01:42:37 +00:00
dependabot[bot] 827c7bcae8 build(deps): bump docker/login-action from 3.6.0 to 3.7.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/5e57cd118135c172c3672efd75eb46360885c0ef...c94ce9fb468520275223c153574b00df6fe4bcc9)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 01:42:36 +00:00
Winfried Plappert bcd4168428 Enhancement: calls to SnapshotFilter.FindLatest() can be simplified (#5688) 2026-01-31 23:04:01 +01:00
Michael Eischer 901235efc9 rewrite: skip snapshot parts not matchable by include patterns 2026-01-31 22:42:02 +01:00
Michael Eischer ef1d525f22 rewriter: return correct error if tree iteration fails 2026-01-31 22:07:07 +01:00
Michael Eischer 74d60ad223 rewriter: test KeepEmptyDirectory option 2026-01-31 22:01:23 +01:00
Michael Eischer 0d71f70a22 minor cleanups and typos 2026-01-31 22:01:23 +01:00
Winfried Plappert ee154ce0ab restic rewrite --include
added function Count() to the *TreeWriter methods
2026-01-31 19:58:29 +00:00
Winfried Plappert b6af01bb28 restic rewrite integration test
convert exclusive lock on repository to 'no-lock'
2026-01-31 19:43:03 +00:00
Winfried Plappert 5148608c39 restic rewrite include - based on restic 0.18.1
cmd/restic/cmd_rewrite.go:
introduction of include filters for this command:
- add include filters, add error checking code
- add new parameter 'keepEmptyDirectoryFunc' to 'walker.NewSnapshotSizeRewriter()',
  so empty directories have to be kept to keep the directory structure intact
- add parameter 'keepEmptySnapshot' to 'filterAndReplaceSnapshot()' to keep snapshots
  intact when nothing is to be included
- introduce helper function 'gatherIncludeFilters()' and 'gatherExcludeFilters()' to
  keep code flow clean

cmd/restic/cmd_rewrite_integration_test.go:
add several new tests around the 'include' functionality

internal/filter/include.go:
this is where is include filter is defined

internal/walker/rewriter.go:
- struct RewriteOpts gains field 'KeepEmtpyDirectory', which is a 'NodeKeepEmptyDirectoryFunc()'
  which defaults to nil, so that al subdirectories are kept
- function 'NewSnapshotSizeRewriter()' gains the parameter 'keepEmptyDirecoryFilter' which
  controls the management of empty subdirectories in case of include filters active

internal/data/tree.go:
gains a function Count() for checking the number if node elements in a newly built tree

internal/walker/rewriter_test.go:
function 'NewSnapshotSizeRewriter()' gets an additional parameter nil to keeps things happy

cmd/restic/cmd_repair_snapshots.go:
function 'filterAndReplaceSnapshot()' gets an additional parameter 'keepEmptySnapshot=nil'

doc/045_working_with_repos.rst:
gets to mention include filters

changelog/unreleased/issue-4278:
the usual announcement file

git rebase master -i produced this

restic rewrite include - keep linter happy

cmd/restic/cmd_rewrite_integration_test.go:
linter likes strings.Contain() better than my strings.Index() >= 0
2026-01-31 19:42:56 +00:00
Michael Eischer 083cdf0675 Merge pull request #5613 from MichaelEischer/tree-node-iterator 2026-01-31 20:10:57 +01:00
Michael Eischer ce7c144aac data: add support for unknown keys to treeIterator
While not planned, it's also not completely impossible that a tree node
might get additional top-level fields. As the tree iterator is built
with a strict expectation of the top-level fields, this would result in
a parsing error. Future-proof the code by simply skipping unknown
fields.
2026-01-31 20:03:38 +01:00
Michael Eischer 81948937ca data: test DualTreeIterator 2026-01-31 20:03:38 +01:00
Michael Eischer fa8889eec4 data: test LoadTree+SaveTree cycle 2026-01-31 20:03:38 +01:00
Michael Eischer 6de64911fb data: test TreeFinder 2026-01-31 20:03:38 +01:00
Michael Eischer 17688c2313 data: move TestTreeMap to data package to allow reuse 2026-01-31 20:03:38 +01:00
Michael Eischer e1a5550a27 test: use generics in Equal function signature
This simplifies comparing a typed value against nil. Previously it was
necessary to case nil into the proper type.
2026-01-31 20:03:38 +01:00
Michael Eischer 24d56fe2a6 diff: switch to efficient DualTreeIterator
The previous implementation stored the whole tree in a map and used it
for checking overlap between trees. This is now replaced with the
DualTreeIterator, which iterates over two trees in parallel and returns
the merge stream in order. In case of overlap between both trees, it
returns both nodes at the same time. Otherwise, only a single node is
returned.
2026-01-31 20:03:38 +01:00
Michael Eischer 350f29d921 data: replace Tree with TreeNodeIterator
The TreeNodeIterator decodes nodes while iterating over a tree blob.
This should reduce peak memory usage as now only the serialized tree
blob and a single node have to alive at the same time. Using the
iterator has implications for the error handling however. Now it is
necessary that all loops that iterate through a tree check for errors
before using the node returned by the iterator.

The other change is that it is no longer possible to iterate over a tree
multiple times. Instead it must be loaded a second time. This only
affects the tree rewriting code.
2026-01-31 20:03:38 +01:00
Michael Eischer 1e183509d4 data: rework StreamTrees to use synchronous callbacks
The tree.Nodes will be replaced by an iterator to loads and serializes
tree node ondemand. Thus, the processing moves from StreamTrees into the
callback. Schedule them onto the workers used by StreamTrees for proper
load distribution.
2026-01-31 20:03:38 +01:00
Michael Eischer 25a5aa3520 dump: fix missing error handling if tree cannot be read 2026-01-31 19:18:36 +01:00
Michael Eischer 278e457e1f data: use data.TreeWriter to serialize&write data.Tree
Always serialize trees via TreeJSONBuilder. Add a wrapper called
TreeWriter which combines serialization and saving the tree blob in the
repository. In the future, TreeJSONBuilder will have to upload tree
chunks while the tree is still serialized. This will a wrapper like
TreeWriter, so add it right now already.

The archiver.treeSaver still directly uses the TreeJSONBuilder as it
requires special handling.
2026-01-31 19:18:36 +01:00
Michael Eischer f84d398989 repository: prevent test deadlock within WithBlobUploader
Calling t.Fatal internally triggers runtime.Goexit . This kills the
current goroutine while only running deferred code. Add an extra context
that gets canceled if the go routine exits while within the user
provided callback.
2026-01-31 19:18:36 +01:00
Michael Eischer d82ea53735 data: fix invalid trees used in test cases
data.TestCreateSnapshot which is used in particular by TestFindUsedBlobs
and TestFindUsedBlobs could generate trees with duplicate file names.
This is invalid and going forward will result in an error.
2026-01-31 19:18:36 +01:00
Michael Eischer 34fdf5ba96 Merge pull request #5636 from MichaelEischer/clarify-parameter-docs 2026-01-31 19:15:24 +01:00
Michael Eischer 70591f00ed Merge pull request #5690 from restic/backend-no-restic-imports 2026-01-31 19:13:07 +01:00
Michael Eischer 4bc6bb7e27 slightly reduce redundant wording 2026-01-31 19:07:06 +01:00
Michael Eischer 2628daba97 CI: prevent backends from importing internal/restic package 2026-01-31 12:00:04 +01:00
Martin Castillo 2269ec82e1 man: add note about append-only mode to restic-unlock.1
Since restic unlock _removes_ locks, a user might wonder
whether this works in append-only mode, which prevents deletion of
data.  This change explicitly mentions in the man page that the deletion
of locks is an allowed exception.
2026-01-27 12:07:47 +01:00
Winfried PlappertandMichael Eischer 86ccc6d445 Bugfix: restic check: add missing finalizeSnapshotFilter() (#5644)
add missing finalizeSnapshotFilter() to cmd.RunE()

---------

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2026-01-26 21:08:15 +00:00
Michael Eischer d0a5d0e2f7 Merge pull request #5657 from restic/dependabot/go_modules/github.com/spf13/cobra-1.10.2
build(deps): bump github.com/spf13/cobra from 1.10.1 to 1.10.2
2026-01-26 21:52:26 +01:00
Michael Eischer fa13f1895f Merge pull request #5658 from restic/dependabot/go_modules/github.com/elithrar/simple-scrypt-1.4.0
build(deps): bump github.com/elithrar/simple-scrypt from 1.3.0 to 1.4.0
2026-01-26 21:48:28 +01:00
Michael Eischer 880b08f9ec Merge pull request #5627 from MichaelEischer/faster-files-writer
restore: tune fileswriter
2026-01-26 21:45:49 +01:00
dependabot[bot] 1368db5777 build(deps): bump github.com/spf13/cobra from 1.10.1 to 1.10.2
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 20:40:43 +00:00
Michael Eischer f78e3f369d Merge pull request #5637 from MichaelEischer/docs-table-width
docs: fix table width
2026-01-26 21:38:34 +01:00
Michael Eischer 39271a9984 Merge pull request #5656 from restic/dependabot/go_modules/cloud.google.com/go/storage-1.58.0
build(deps): bump cloud.google.com/go/storage from 1.57.2 to 1.58.0
2026-01-26 21:37:24 +01:00
Michael Eischer 2c1e8a0412 Merge pull request #5655 from restic/dependabot/go_modules/github.com/klauspost/compress-1.18.2
build(deps): bump github.com/klauspost/compress from 1.18.1 to 1.18.2
2026-01-26 21:36:53 +01:00
Michael Eischer 155372404a Merge pull request #5654 from restic/dependabot/go_modules/golang-x-deps-f1409dc592
build(deps): bump the golang-x-deps group with 7 updates
2026-01-26 21:36:18 +01:00
Ilya Grigoriev 79c37f3d1a ui: mention compressed size of added files in backup -vv
This is already shown for modified files, but the added files message
wasn't updated when compression was implemented in restic.
2026-01-15 18:39:16 -08:00
dependabot[bot] 80531dbe53 build(deps): bump github.com/elithrar/simple-scrypt from 1.3.0 to 1.4.0
Bumps [github.com/elithrar/simple-scrypt](https://github.com/elithrar/simple-scrypt) from 1.3.0 to 1.4.0.
- [Release notes](https://github.com/elithrar/simple-scrypt/releases)
- [Commits](https://github.com/elithrar/simple-scrypt/compare/v1.3.0...v1.4.0)

---
updated-dependencies:
- dependency-name: github.com/elithrar/simple-scrypt
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 01:02:15 +00:00
dependabot[bot] 40fe9f34e7 build(deps): bump cloud.google.com/go/storage from 1.57.2 to 1.58.0
Bumps [cloud.google.com/go/storage](https://github.com/googleapis/google-cloud-go) from 1.57.2 to 1.58.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/storage/v1.57.2...spanner/v1.58.0)

---
updated-dependencies:
- dependency-name: cloud.google.com/go/storage
  dependency-version: 1.58.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 01:02:06 +00:00
dependabot[bot] 4d0ec87f35 build(deps): bump github.com/klauspost/compress from 1.18.1 to 1.18.2
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.1 to 1.18.2.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.1...v1.18.2)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 01:01:51 +00:00
dependabot[bot] d6f376b6c8 build(deps): bump the golang-x-deps group with 7 updates
Bumps the golang-x-deps group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.45.0` | `0.46.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.47.0` | `0.48.0` |
| [golang.org/x/oauth2](https://github.com/golang/oauth2) | `0.33.0` | `0.34.0` |
| [golang.org/x/sync](https://github.com/golang/sync) | `0.18.0` | `0.19.0` |
| [golang.org/x/sys](https://github.com/golang/sys) | `0.38.0` | `0.39.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.37.0` | `0.38.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.31.0` | `0.32.0` |


Updates `golang.org/x/crypto` from 0.45.0 to 0.46.0
- [Commits](https://github.com/golang/crypto/compare/v0.45.0...v0.46.0)

Updates `golang.org/x/net` from 0.47.0 to 0.48.0
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.48.0)

Updates `golang.org/x/oauth2` from 0.33.0 to 0.34.0
- [Commits](https://github.com/golang/oauth2/compare/v0.33.0...v0.34.0)

Updates `golang.org/x/sync` from 0.18.0 to 0.19.0
- [Commits](https://github.com/golang/sync/compare/v0.18.0...v0.19.0)

Updates `golang.org/x/sys` from 0.38.0 to 0.39.0
- [Commits](https://github.com/golang/sys/compare/v0.38.0...v0.39.0)

Updates `golang.org/x/term` from 0.37.0 to 0.38.0
- [Commits](https://github.com/golang/term/compare/v0.37.0...v0.38.0)

Updates `golang.org/x/text` from 0.31.0 to 0.32.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/net
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/sync
  dependency-version: 0.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/sys
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/term
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/text
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 01:01:44 +00:00
fabien-joubert 8179c4f676 docs: add warning for capability-based non-root backups 2025-12-27 22:38:16 +01:00
Kjetil Torgrim Homme ee099e4662 doc: restore: simplify restore target name
the suffix "-work" is visually distracting since it copies a part of the
directory path being restored.  this makes it easy to gloss over where
the "work" string actually comes from.  as a bonus, it makes the examples
slightly shorter to read.
2025-12-05 18:07:11 +01:00
Kjetil Torgrim Homme 655e527765 doc: restore: be clearer about resulting filenames in restore 2025-12-05 18:04:46 +01:00
Kjetil Torgrim Homme d34ba9b6bd doc: restore: explain how --delete and --exclude work together 2025-12-05 17:59:56 +01:00
Kjetil Torgrim Homme 0673b58d7c doc: restore: explain explicitly effect of specifying snapshot:subfolder
The effect could be read from the examples, but it was easy to miss.
2025-12-05 17:50:53 +01:00
Michael Eischer 9e2d60e28c Merge pull request #5632 from restic/dependabot/go_modules/github.com/minio/minio-go/v7-7.0.97
build(deps): bump github.com/minio/minio-go/v7 from 7.0.95 to 7.0.97
2025-12-03 21:34:27 +01:00
Michael Eischer ebc51e60c9 Merge pull request #5626 from MichaelEischer/lazy-status
ui: only redraw status bar if it has not changed
2025-12-03 21:29:35 +01:00
dependabot[bot] a9a13afcec build(deps): bump github.com/minio/minio-go/v7 from 7.0.95 to 7.0.97
Bumps [github.com/minio/minio-go/v7](https://github.com/minio/minio-go) from 7.0.95 to 7.0.97.
- [Release notes](https://github.com/minio/minio-go/releases)
- [Commits](https://github.com/minio/minio-go/compare/v7.0.95...v7.0.97)

---
updated-dependencies:
- dependency-name: github.com/minio/minio-go/v7
  dependency-version: 7.0.97
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 20:24:30 +00:00
Michael Eischer d7b87cedbc Merge pull request #5630 from restic/dependabot/go_modules/github.com/ncw/swift/v2-2.0.5
build(deps): bump github.com/ncw/swift/v2 from 2.0.4 to 2.0.5
2025-12-03 21:23:18 +01:00
Michael Eischer a8be8e36fa Merge pull request #5621 from MichaelEischer/copy-stream-snapshots
copy: iterate through snapshots
2025-12-03 21:21:05 +01:00
dependabot[bot] 74f72ec707 build(deps): bump github.com/ncw/swift/v2 from 2.0.4 to 2.0.5
Bumps [github.com/ncw/swift/v2](https://github.com/ncw/swift) from 2.0.4 to 2.0.5.
- [Release notes](https://github.com/ncw/swift/releases)
- [Changelog](https://github.com/ncw/swift/blob/master/RELEASE.md)
- [Commits](https://github.com/ncw/swift/compare/v2.0.4...v2.0.5)

---
updated-dependencies:
- dependency-name: github.com/ncw/swift/v2
  dependency-version: 2.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 20:06:29 +00:00
Michael Eischer 0b0b714b84 Merge pull request #5628 from MichaelEischer/cleanup-old-build-lines
remove old // +build comments
2025-12-03 21:06:11 +01:00
Michael Eischer 3df4582b2b Merge pull request #5635 from restic/dependabot/github_actions/golangci/golangci-lint-action-9
build(deps): bump golangci/golangci-lint-action from 8 to 9
2025-12-03 21:03:55 +01:00
Michael Eischer a24184357e Merge pull request #5634 from restic/dependabot/github_actions/actions/checkout-6
build(deps): bump actions/checkout from 5 to 6
2025-12-03 21:00:50 +01:00
Michael Eischer 0d024ad046 Merge pull request #5631 from restic/dependabot/go_modules/github.com/Azure/azure-sdk-for-go/sdk/azidentity-1.13.1
build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity from 1.13.0 to 1.13.1
2025-12-03 20:58:55 +01:00
Michael Eischer 3efd7b5fd0 Merge pull request #5629 from restic/dependabot/go_modules/github.com/klauspost/compress-1.18.1
build(deps): bump github.com/klauspost/compress from 1.18.0 to 1.18.1
2025-12-03 20:58:48 +01:00
Michael Eischer 4fd9bfc32b docs: fix table width 2025-12-03 20:38:21 +01:00
Michael Eischer 7a3b06f78a docs: move environment variables to the scripting section 2025-12-03 18:38:36 +01:00
Michael Eischer a58d176500 docs: clarify that parameter tuning applies to all commands 2025-12-03 18:33:00 +01:00
dependabot[bot] 0af1257184 build(deps): bump golangci/golangci-lint-action from 8 to 9
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 8 to 9.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v8...v9)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 01:14:24 +00:00
dependabot[bot] a3f1c65022 build(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 01:14:22 +00:00
dependabot[bot] fa4ca9b5b4 build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity
Bumps [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) from 1.13.0 to 1.13.1.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.13.0...sdk/azidentity/v1.13.1)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azidentity
  dependency-version: 1.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 01:04:00 +00:00
dependabot[bot] ebdeecde42 build(deps): bump github.com/klauspost/compress from 1.18.0 to 1.18.1
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.0...v1.18.1)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 01:03:43 +00:00
Michael Eischer 1e6ed458ff remove old // +build comments 2025-11-30 11:53:23 +01:00
Michael Eischer 760d0220f4 restorer: scale file cache with workers count 2025-11-30 11:01:01 +01:00
Michael Eischer 24fcfeafcb restore: cache file descriptors
This avoid opening and closing files after each single blob write
2025-11-30 10:56:15 +01:00
Michael Eischer 0ee9360f3e restore: reduce contention while writing files 2025-11-29 23:09:04 +01:00
Michael Eischer ae6d6bd9a6 ui: only redraw status bar if it has not changed 2025-11-29 22:09:41 +01:00
Aneesh NandAneesh Nireshwalia b9afdf795e Fix: Correctly restore ACL inheritance state (#5465)
* Fix: Correctly restore ACL inheritance state

When restoring a file or directory on Windows, the `IsInherited` property of its Access Control Entries (ACEs) was always being set to `False`, even if the ACEs were inherited in the original backup.

This was caused by the restore process calling the `SetNamedSecurityInfo` API without providing context about the object's inheritance policy. By default, this API applies the provided Discretionary Access Control List (DACL) as an explicit set of permissions, thereby losing the original inheritance state.

This commit fixes the issue by inspecting the `Control` flags of the saved Security Descriptor during restore. Based on whether the `SE_DACL_PROTECTED` flag is present, the code now adds the appropriate `PROTECTED_DACL_SECURITY_INFORMATION` or `UNPROTECTED_DACL_SECURITY_INFORMATION` flag to the `SetNamedSecurityInfo` API call.

By providing this crucial inheritance context, the Windows API can now correctly reconstruct the ACL, ensuring the `IsInherited` status of each ACE is preserved as it was at the time of backup.

* Fix: Correctly restore ACL inheritance flags

This commit resolves an issue where the ACL inheritance state (`IsInherited` property) was not being correctly restored for files and directories on Windows.

The root cause was that the `SECURITY_INFORMATION` flags used in the `SetNamedSecurityInfo` API call contained both the `PROTECTED_DACL_SECURITY_INFORMATION` and `UNPROTECTED_DACL_SECURITY_INFORMATION` flags simultaneously. When faced with this conflicting information, the Windows API defaulted to the more restrictive `PROTECTED` behavior, incorrectly disabling inheritance on restored items.

The fix modifies the `setNamedSecurityInfoHigh` function to first clear all existing inheritance-related flags from the `securityInfo` bitmask. It then adds the single, correct flag (`PROTECTED` or `UNPROTECTED`) based on the `SE_DACL_PROTECTED` control bit from the original, saved Security Descriptor.

This ensures that the API receives unambiguous instructions, allowing it to correctly preserve the inheritance state as it was at the time of backup. The accompanying test case for ACL inheritance now passes with this change.

* Fix inheritance flag handling in low-privilege security descriptor restore

When restoring files without admin privileges, the IsInherited property
of Access Control Entries (ACEs) was not being preserved correctly.
The low-privilege restore path (setNamedSecurityInfoLow) was using a
static PROTECTED_DACL_SECURITY_INFORMATION flag, which always marked
the restored DACL as explicitly set rather than inherited.

This commit updates setNamedSecurityInfoLow to dynamically determine
the correct inheritance flag based on the SE_DACL_PROTECTED control
flag from the original security descriptor, matching the behavior of
the high-privilege path (setNamedSecurityInfoHigh).

Changes:
- Update setNamedSecurityInfoLow to accept control flags parameter
- Add logic to set either PROTECTED_DACL_SECURITY_INFORMATION or
  UNPROTECTED_DACL_SECURITY_INFORMATION based on the original SD
- Add TestRestoreSecurityDescriptorInheritanceLowPrivilege to verify
  inheritance is correctly restored in low-privilege scenarios

This ensures that both admin and non-admin restore operations correctly
preserve the inheritance state of ACLs, maintaining the original
permissions flow on child objects.

Addresses review feedback on PR for issue #5427

* Refactor security flags into separate backup/restore variants

Split highSecurityFlags into highBackupSecurityFlags and
highRestoreSecurityFlags to avoid runtime bitwise operations.
This makes the code cleaner and more maintainable by using
appropriate flags for GET vs SET operations.

Addresses review feedback on PR for issue #5427

---------

Co-authored-by: Aneesh Nireshwalia <anireshw@akamai.com>
2025-11-28 19:22:47 +00:00
Winfried PlappertandMichael Eischer ce57961f14 restic check with snapshot filters (#5469)
---------

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2025-11-28 19:12:38 +00:00
Michael Eischer e1bc2fb71a copy: iterate through snapshots 2025-11-26 22:48:54 +01:00
Michael Eischer 8fdbdc57a0 Merge pull request #5581 from restic/dependabot/go_modules/google.golang.org/api-0.254.0
build(deps): bump google.golang.org/api from 0.248.0 to 0.254.0
2025-11-26 22:24:43 +01:00
dependabot[bot] 69ac0d84ac build(deps): bump google.golang.org/api from 0.248.0 to 0.254.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.248.0 to 0.254.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.248.0...v0.254.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-version: 0.254.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-26 21:12:48 +00:00
Michael Eischer 0a96f0d623 Merge pull request #5578 from restic/dependabot/go_modules/cloud.google.com/go/storage-1.57.1
build(deps): bump cloud.google.com/go/storage from 1.56.1 to 1.57.1
2025-11-26 22:12:09 +01:00
Michael Eischer 0d8b715d92 Merge pull request #5547 from restic/dependabot/go_modules/golang-x-deps-3a742399ff
build(deps): bump the golang-x-deps group with 8 updates
2025-11-26 22:11:35 +01:00
dependabot[bot] 31e3717b25 build(deps): bump the golang-x-deps group with 8 updates
Bumps the golang-x-deps group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.41.0` | `0.42.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.43.0` | `0.44.0` |
| [golang.org/x/oauth2](https://github.com/golang/oauth2) | `0.30.0` | `0.31.0` |
| [golang.org/x/sync](https://github.com/golang/sync) | `0.16.0` | `0.17.0` |
| [golang.org/x/sys](https://github.com/golang/sys) | `0.35.0` | `0.36.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.34.0` | `0.35.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.28.0` | `0.29.0` |
| [golang.org/x/time](https://github.com/golang/time) | `0.12.0` | `0.13.0` |


Updates `golang.org/x/crypto` from 0.41.0 to 0.42.0
- [Commits](https://github.com/golang/crypto/compare/v0.41.0...v0.42.0)

Updates `golang.org/x/net` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/net/compare/v0.43.0...v0.44.0)

Updates `golang.org/x/oauth2` from 0.30.0 to 0.31.0
- [Commits](https://github.com/golang/oauth2/compare/v0.30.0...v0.31.0)

Updates `golang.org/x/sync` from 0.16.0 to 0.17.0
- [Commits](https://github.com/golang/sync/compare/v0.16.0...v0.17.0)

Updates `golang.org/x/sys` from 0.35.0 to 0.36.0
- [Commits](https://github.com/golang/sys/compare/v0.35.0...v0.36.0)

Updates `golang.org/x/term` from 0.34.0 to 0.35.0
- [Commits](https://github.com/golang/term/compare/v0.34.0...v0.35.0)

Updates `golang.org/x/text` from 0.28.0 to 0.29.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.28.0...v0.29.0)

Updates `golang.org/x/time` from 0.12.0 to 0.13.0
- [Commits](https://github.com/golang/time/compare/v0.12.0...v0.13.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/net
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/sync
  dependency-version: 0.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/sys
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/term
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/text
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
- dependency-name: golang.org/x/time
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-26 20:56:35 +00:00
dependabot[bot] 42133ccffe build(deps): bump cloud.google.com/go/storage from 1.56.1 to 1.57.1
Bumps [cloud.google.com/go/storage](https://github.com/googleapis/google-cloud-go) from 1.56.1 to 1.57.1.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/storage/v1.56.1...storage/v1.57.1)

---
updated-dependencies:
- dependency-name: cloud.google.com/go/storage
  dependency-version: 1.57.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-26 20:56:27 +00:00
Michael Eischer 77374b5bf0 Merge pull request #5619 from restic/bump-go-version
bump minimum go version to 1.24
2025-11-26 21:55:02 +01:00
Michael Eischer f3a89bfff6 Merge pull request #5612 from MichaelEischer/repository-async-saveblob
repository: add async blob upload method
2025-11-26 21:34:35 +01:00
Michael Eischer 7696e4b495 bump minimum go version to 1.24 2025-11-26 21:33:40 +01:00
Michael Eischer 5cc8636047 Merge pull request #5614 from MichaelEischer/fix-lookupblobsize
repository: fix LookupBlobSize to also return pending blobs
2025-11-26 21:24:32 +01:00
Michael Eischer 6769d26068 archiver: improve test reliability 2025-11-26 21:21:16 +01:00
Michael Eischer 5607fd759f repository: fix race condition for blobSaver shutdown
wg.Go() may not be called after wg.Wait(). This prevents connecting two
errgroups such that the errors are propagated between them if the child
errgroup dynamically starts goroutines. Instead use just a single errgroup,
and sequence the shutdown using a sync.WaitGroup. This is far simpler
and does not require any "clever" tricks.
2025-11-26 21:18:22 +01:00
Michael Eischer 9f87e9096a repository: add tests for SaveBlobAsync 2025-11-26 21:18:22 +01:00
Michael Eischer d8dcd6d115 archiver: add buffer test 2025-11-26 21:18:22 +01:00
Michael Eischer 3f92987974 archiver: assert number of uploaded chunks in fileSaver test 2025-11-26 21:18:22 +01:00
Michael Eischer 7f6fdcc52c archiver: convert buffer pool to use sync.Pool 2025-11-26 21:18:22 +01:00
Michael Eischer dd6cb0dd8e archiver: port to repository.SaveBlobAsync 2025-11-26 21:18:22 +01:00
Michael Eischer 046b0e711d repository: add SaveBlobAsync method 2025-11-26 21:18:21 +01:00
Michael Eischer 4d2da63829 Merge pull request #5610 from MichaelEischer/associated-blob-set-everywhere
check/copy/diff/stats: reduce memory usage
2025-11-26 21:09:26 +01:00
Michael Eischer 134893bd35 copy: use AssociatedBlobSet to keep track of processed trees 2025-11-26 21:00:18 +01:00
Michael Eischer 7b59dd7cf4 add changelog 2025-11-26 20:59:39 +01:00
Michael Eischer 84dda4dc74 check: use AssociatedBlobSet 2025-11-26 20:59:39 +01:00
Michael Eischer 46ebee948f stats: use AssociatedBlobSet 2025-11-26 20:59:39 +01:00
Michael Eischer d91fe1d7e1 diff: use AssociatedBlobSet 2025-11-26 20:59:39 +01:00
Michael Eischer ff099a216a copy: use AssociatedBlobSet 2025-11-26 20:59:38 +01:00
Michael Eischer 07d090f233 repository: expose AssociatedBlobSet via repository interface 2025-11-26 20:59:08 +01:00
Michael Eischer 0f05277b47 index: add sub and intersect method to AssociatedSet 2025-11-26 20:59:08 +01:00
Michael Eischer 7e80536a9b Merge pull request #5472 from wplapper/cmd_copy_stream
restic copy --stream: run one large copy operation crossing snapshot boundaries - issue #5453
2025-11-26 20:57:46 +01:00
Michael Eischer f9e5660e75 output which source and target snapshot belong together 2025-11-23 22:01:53 +01:00
Michael Eischer e79b01d82f more aggressive batching 2025-11-23 21:46:03 +01:00
Michael Eischer 857b42fca4 merge into existing copy test 2025-11-23 19:08:49 +01:00
Michael Eischer 39db78446f Simplify test 2025-11-23 19:05:55 +01:00
Michael Eischer f1aabdd293 index: add test for pending blobs 2025-11-23 18:08:56 +01:00
Michael Eischer 50d376c543 repository: fix LookupBlobSize to also report pending blobs 2025-11-23 17:55:13 +01:00
Michael Eischer 7d08c9282a align docs 2025-11-23 17:51:07 +01:00
Michael Eischer cf409b7c66 automatically batch snapshots in copy 2025-11-23 17:40:37 +01:00
Michael Eischer f95dc73d38 deduplicate blob enqueuing 2025-11-23 17:13:10 +01:00
Michael Eischer 63bc1405ea unify snapshot copy codepaths 2025-11-23 17:12:54 +01:00
Michael Eischer 405813f250 repository: fix LookupBlobSize to also report pending blobs 2025-11-23 17:09:07 +01:00
Michael Eischer 05364500b6 use correct context 2025-11-23 16:25:09 +01:00
Michael Eischer e775192fe7 don't sort snapshots, drop duplicate code and cleanup copyTreeBatched function signature 2025-11-23 16:20:40 +01:00
Michael Eischer 4395a77154 copy: remove bugous seenBlobs set 2025-11-23 16:06:45 +01:00
Michael Eischer 81d8bc4ade repository: replace CopyBlobs with Repack implementation 2025-11-23 16:06:29 +01:00
Michael Eischer d681b8af5e Merge pull request #5611 from insertish/docs/scripting-tag-schema
docs: correct the schema provided for tag summary
2025-11-23 15:45:53 +01:00
izzy 629eaa5d21 docs: correct the schema provided for tag summary 2025-11-20 17:35:25 +00:00
Michael Eischer 6174c91042 Merge pull request #5588 from seqizz/g_timezoneshow
snapshots: Show timezone in non-compact output
2025-11-19 22:06:37 +01:00
Winfried Plappert b24b088978 restic copy --batch: The mighty linter
I cave in - no double comment
2025-11-19 07:34:39 +00:00
Winfried Plappert fc3de018bc restic copy --batch - fussy linter
internal/repository/repack.go: I have to please the mighty linter.
2025-11-19 07:29:09 +00:00
Winfried Plappert b87f7586e4 restic copy --batch: a fresh start from commit 382616747
Instead of rebasing my code, I decided to start fresh, since WithBlobUploader()
has been introduced.

changelog/unreleased/issue-5453:
doc/045_working_with_repos.rst:
the usual

cmd/restic/cmd_copy.go:
gather all snaps to be collected - collectAllSnapshots()
run overall copy step - func copyTreeBatched()
helper copySaveSnapshot() to save the corresponding snapshot

internal/repository/repack.go:
introduce wrapper CopyBlobs(), which passes parameter `uploader restic.BlobSaver` from
WithBlobUploader() via copyTreeBatched() to repack().

internal/backend/local/local_windows.go:
I did not touch it, but gofmt did: whitespace
2025-11-19 07:09:24 +00:00
Gürkan dc4e9b31f6 snapshots: Show timezone in non-compact output 2025-11-18 13:32:44 +01:00
Michael Eischer 8767549367 Merge pull request #5601 from MichaelEischer/snapshots-fix-groupby-with-latest
snapshots: correctly handle --latest in combination with --group-by
2025-11-17 22:50:50 +01:00
Michael Eischer 5afe61585b snapshots: correctly handle --latest in combination with --group-by 2025-11-17 22:26:57 +01:00
Michael Eischer 46f3ece883 Merge pull request #5597 from MichaelEischer/bump-go-for-standalone-docker
bump go version in dockerfile to go 1.25
2025-11-17 22:05:45 +01:00
Michael Eischer 96adbbaa42 Merge pull request #5599 from MichaelEischer/prune-clean-error
prune: return proper error if blob cannot be found
2025-11-17 22:05:17 +01:00
Michael Eischer 7297047b71 Merge pull request #5600 from MichaelEischer/docs-tmp-var-on-windows
only suggest TMP as tmp dir variable on windows
2025-11-17 22:04:42 +01:00
Michael Eischer 132f2f8a23 Merge pull request #5602 from MichaelEischer/fix-flaky-rclone-test
rclone: fix rare test failure if rclone cannot be started
2025-11-17 22:04:10 +01:00
Michael Eischer a519d1e8df Merge pull request #5603 from MichaelEischer/debug-flaky-windows-test
restore: enable debug logging for flaky windows test
2025-11-17 22:03:38 +01:00
Paulo SaraivaandPaulo Saraiva c1a89d5150 Allow for a personal token to be specified for self-updates (#5568)
* Allow for a personal token to be specified for self-updates

This change will allow for setting the $GITHUB_ACCESS_TOKEN environment variable with a Github personal access token, allowing e.g. for higher rate limits

* Refactor github request and add test

---------

Co-authored-by: Paulo Saraiva <pauloman@cern.ch>
2025-11-17 21:39:39 +01:00
Michael Eischer 3826167474 Merge pull request #5424 from Crazycatz00/sebackup-fixes
Windows Backup Privilege Tweaks
2025-11-16 21:35:35 +01:00
Michael Eischer 98f56d8ada restore: enable debug logging for flaky windows test 2025-11-16 20:24:19 +01:00
Michael Eischer 1caeb2aa4d rclone: fix rare test failure if rclone cannot be started 2025-11-16 20:14:21 +01:00
crazycatz00 3ab68d4d11 fs: Clarified documentation 2025-11-16 11:53:13 -05:00
Michael Eischer ffc5e9bd5c only suggest TMP as tmp dir variable on windows
TMP takes precedence over TEMP.
2025-11-16 17:31:36 +01:00
Michael Eischer 0ff3e20c4b prune: return proper error if blob cannot be found 2025-11-16 17:04:03 +01:00
Michael Eischer 3b854d9c04 Merge pull request #5449 from provokateurin/restore-ownership-by-name
feat(internal/fs/node): Restore ownership by name
2025-11-16 16:50:36 +01:00
ferringbandMichael Eischer 87f26accb7 feat: add integrated nice and ionice options for docker (#5448)
The intended usage here is to basically kick restic as a background
"do it, but don't bother my normal load" process.

This allows passing the following environment variables in to
influence scheduling:

- NICE: usual CPU nice.  Defaults to 0.  This requires CAP_SYS_NICE
  to set a negative nice (IE, prioritize).
- IONICE_CLASS: usual ionice class.  Note that setting realtime
  requires CAP_SYS_ADMIN.  Also note the actual ionice default
  is "none".
- IONICE_PRIORITY: set the priority within the given class.  Ignored
  if no class is specified due to class default of "no scheduler".

---------

Signed-off-by: Brian Harring <ferringb@gmail.com>
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2025-11-16 16:42:33 +01:00
provokateurin 8fae46011a feat(internal/fs/node): Restore ownership by name 2025-11-16 16:40:58 +01:00
Michael Eischer c854338ad1 Merge pull request #5596 from mikix/chmod-again
backend/local: fix "operation not supported" when unlocking
2025-11-16 14:25:04 +01:00
Michael Eischer 10a10b8d63 bump go version in dockerfile to go 1.25
Note that this go version is independent of that used for the official
release binaries.
2025-11-16 14:22:43 +01:00
Michael Terry 7f3e3b77ce backend/local: fix "operation not supported" when unlocking
If the repo is on a mounted folder that doesn't support chmod (like
SMB), it was causing an "operation not supported" error when trying to
chmod 666 a file before deleting it.

But it isn't generally needed before deleting a file (the folder
permissions matter there, not the file permissions). So, just drop it.
2025-11-16 08:09:51 -05:00
Michael Eischer d81f95c777 Merge pull request #5464 from wplapper/cmd_copy_v2
restic copy - add more status counters - issue #5175
2025-11-16 13:55:41 +01:00
DoS007andMichael Eischer 2bd6649813 docs: add info about ssd wear in backend connections (#5496)
---------

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2025-11-16 12:48:56 +00:00
Winfried Plappert 3b71c44755 restic copy - stattistics counters
fixed typo in changelog/unreleased/pull-5319
2025-11-16 13:47:11 +01:00
Winfried Plappert 1e3b96bf99 restic copy - statistics feature
reword the description od the PR
2025-11-16 13:47:11 +01:00
Winfried Plappert 25611f4628 restic copy - add statistics counters
cmd/restic/cmd_copy.go:
add function copyStats() and call it before the actual copying starts.

changelog/unreleased/pull-5319:
rephrased wording of the statistics counters.
2025-11-16 13:47:10 +01:00
Winfried Plappert 90ac3efa88 restic copy - add additional status counters
'copyTree()' now counts and sizes the blobs in 'copyBlobs' and prints them out
via 'Verbosef()'.
2025-11-16 13:46:27 +01:00
Michael Eischer 5b173d2206 Merge pull request #5567 from Paulomen2712/add_better_forget_example_docs
Improve example for forget --keep-daily
2025-11-16 13:45:13 +01:00
Michael Eischer 14f3bc8232 Merge pull request #5560 from MichaelEischer/index-iterators
index: port to  modern Go iterators
2025-11-16 13:24:48 +01:00
Michael Eischer 4ef7b4676b Merge pull request #5559 from MichaelEischer/cleanup-repack
repository: remove unused return value from Repack
2025-11-16 13:01:00 +01:00
Michael Eischer b587c126e0 Fix linter warning 2025-11-16 12:56:37 +01:00
Michael Eischer 9944ef7a7c index: convert AssociatedSet to go iterators 2025-11-16 12:56:37 +01:00
Michael Eischer 38c543457e index: convert to implement modern go iterators 2025-11-16 12:56:37 +01:00
Michael Eischer 393e49fc89 repository: update comment 2025-11-16 12:51:46 +01:00
Michael Eischer a0925fa922 repository: set progress bar maximum in Repack 2025-11-16 12:51:46 +01:00
Michael Eischer b2afccbd96 repository: remove unused obsoletePacks return values from Repack 2025-11-16 12:51:46 +01:00
Michael Eischer 0624b656b8 Merge pull request #5558 from MichaelEischer/simplify-blob-upload
repository: enforce correct usage of SaveBlob
2025-11-16 12:51:01 +01:00
Brook fadeb03f84 Update Nix/NixOS installation instructions (#5591)
Corrected spelling errors and updated installation instructions for Nix/NixOS.
2025-11-16 11:31:47 +00:00
Michael Eischer fc06a79518 Merge pull request #5579 from restic/dependabot/go_modules/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob-1.6.3
build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob from 1.6.2 to 1.6.3
2025-11-16 12:03:02 +01:00
Michael Eischer d5977deb49 Merge pull request #5580 from restic/dependabot/go_modules/github.com/pkg/sftp-1.13.10
build(deps): bump github.com/pkg/sftp from 1.13.9 to 1.13.10
2025-11-16 12:02:30 +01:00
Michael Eischer e3b7bbd020 Merge pull request #5552 from ferringb/dockerignore
add a dockerignore
2025-11-16 12:00:52 +01:00
Michael Eischer 157f174dd9 Merge pull request #5370 from hashier/feat/exclude-macOS-cloud-files
feat(backup): add possibility to exclude macOS cloud-only files
2025-11-16 11:57:37 +01:00
Alex Xu bcc5417dc8 Merge pull request #5386 from Hello71/patch-2
doc: Add ambient caps example, edit file caps
2025-11-16 11:54:43 +01:00
crazycatz00 d14823eb81 fs: Attempt to enable file system privileges on initialization.
Add tests to verify privileges' effects.
2025-11-07 19:31:59 -05:00
crazycatz00 01bf8977e7 fs: Use backup privileges when reading extended attributes for files too. 2025-11-07 19:31:57 -05:00
dependabot[bot] f5a18a7799 build(deps): bump github.com/pkg/sftp from 1.13.9 to 1.13.10
Bumps [github.com/pkg/sftp](https://github.com/pkg/sftp) from 1.13.9 to 1.13.10.
- [Release notes](https://github.com/pkg/sftp/releases)
- [Commits](https://github.com/pkg/sftp/compare/v1.13.9...v1.13.10)

---
updated-dependencies:
- dependency-name: github.com/pkg/sftp
  dependency-version: 1.13.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 01:02:06 +00:00
dependabot[bot] f756c6a441 build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
Bumps [github.com/Azure/azure-sdk-for-go/sdk/storage/azblob](https://github.com/Azure/azure-sdk-for-go) from 1.6.2 to 1.6.3.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/storage/azblob/v1.6.2...sdk/storage/azblob/v1.6.3)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
  dependency-version: 1.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 01:02:03 +00:00
Brian Harring 8cbca05853 add a dockerignore
This is strictly for tightening the container to be more hygenic.

Signed-off-by: Brian Harring <ferringb@gmail.com>
2025-10-29 16:18:40 +01:00
Paulo Manuel Ferreira Dos Santos Saraiva b0eb3652b8 Improve example for forget --keep-daily 2025-10-22 11:31:50 +02:00
Michael Eischer 71432c7f4b Merge pull request #5555 from MichaelEischer/extract-globaloptions
Split globalOptions into separate package
2025-10-12 18:31:44 +02:00
Michael Eischer c6e33c3954 repository: enforce that SaveBlob is called within WithBlobUploader
This is achieved by removing SaveBlob from the public API and only
returning it via a uploader object that is passed in by
WithBlobUploader.
2025-10-12 18:26:26 +02:00
Michael Eischer 1ef785daa3 Merge pull request #5544 from zmanda/fix-gh-5531-azure-backend-upgrade-service-version
azure: use PutBlob API for uploads instead of PutBlock API + PutBlock List API
2025-10-12 18:24:33 +02:00
Michael Eischer aa0fb0210a Merge pull request #5556 from greatroar/cleanup
ui/backup: Prepend, then sort (micro-optimization)
2025-10-12 18:22:36 +02:00
Michael Eischer b6aef592f5 global: split CreateRepository and OpenRepository into smaller functions 2025-10-12 18:20:45 +02:00
Michael Eischer 588c40aaef global: unexport ReadPassword and ReadRepo 2025-10-12 18:08:26 +02:00
Michael Eischer aa7bd241d9 init: move more logic into global package 2025-10-12 18:08:26 +02:00
Michael Eischer 536a2f38bd Merge pull request #5554 from MichaelEischer/termstatus-flush
termstatus: flush before reading password from terminal
2025-10-12 17:59:03 +02:00
Michael Eischer a816b827cf extract GlobalOptions into internal/global package
Rough steps:
```
mv cmd/restic/global* cmd/restic/secondary_repo* internal/global/
sed -i "s/package main/package global/" internal/global/*.go
Rename "GlobalOptions" to "Options" in internal/global/
Replace everywhere " GlobalOptions" -> " global.Options"
Replace everywhere "\*GlobalOptions" -> " *global.Options"
Make SecondaryRepoOptions public
Make create public
Make version public
```
2025-10-12 17:56:28 +02:00
Michael Eischer 2c677d8db4 global: make private fields public 2025-10-12 17:56:28 +02:00
Michael Eischer 394c8de502 add package to create a prepopulated backend registry 2025-10-12 17:56:28 +02:00
Michael Eischer a632f490fa Merge pull request #5550 from MichaelEischer/refactor-check-data-selection
check: refactor pack selection for read data
2025-10-12 17:51:00 +02:00
Michael Eischer 718b97f37f Merge pull request #5551 from restic/slower-terminal-output
Reduce terminal progress fps to 10
2025-10-12 17:47:27 +02:00
Michael Eischer ac4642b479 repository: replace StartPackUploader+Flush with WithBlobUploader
The new method combines both step into a single wrapper function. Thus
it ensures that both are always called in pairs. As an additional
benefit this slightly reduces the boilerplate to upload blobs.
2025-10-08 22:49:45 +02:00
greatroar 20b38010e1 ui/backup: Prepend, then sort (micro-optimization) 2025-10-06 16:16:37 +02:00
Srigovind Nayak f9ff2301e8 changelog: add a changelog entry for azure PutBlob API changes 2025-10-05 21:48:02 +05:30
Srigovind Nayak e65ee3cba8 fix: keep the PutBlock Size to 100 MiB
No complaints in the past.
2025-10-05 21:41:26 +05:30
Srigovind Nayak 34a94afc48 azure: update upload size constants to reduce memory allocation 2025-10-05 21:41:25 +05:30
Srigovind Nayak 9bcd09bde0 azure: reduce singleBlockMaxSize to accommodate 32-bit systems 2025-10-05 21:41:25 +05:30
Srigovind Nayak e80e832130 azure: remove saveSmall, use only PutBlob API 2025-10-05 21:41:25 +05:30
Srigovind Nayak dd2d562b7b azure: enhanced upload with single PutBlob API and configurable upload methods 2025-10-05 21:41:25 +05:30
Michael Eischer e320ef0a62 add changelog 2025-10-05 16:14:16 +02:00
Michael Eischer 30ed992af9 termstatus: flush output before returning OutputRaw() writer
This prevents mangling the output due to delayed messages.
2025-10-05 16:14:16 +02:00
Srigovind NayakandMichael Eischer 481fcb9ca7 backup: return exit code 3 if not all targets are available (#5347)
to make the exit code behaviour consistent with files inaccessible during the backup phase, making this change to exit with code 3 if not all target files/folders are accessible for backup

---------

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2025-10-05 15:38:52 +02:00
Srigovind Nayak 22f254c9ca feat: allow override env RESTIC_HOST with flag to filter all snapshots (#5541) 2025-10-05 13:22:50 +02:00
Michael Eischer f17027eeaa termstatus: flush before reading password from terminal 2025-10-04 23:06:57 +02:00
Christopher Loessl f3d95893b2 feat(backup): add possibility to exclude macOS cloud-only files 2025-10-04 19:22:51 +02:00
Michael Eischer 4759e58994 Reduce terminal progress fps to 10 2025-10-04 17:34:40 +02:00
Winfried PlappertandMichael Eischer a2a49cf784 list integration test: error scanning 'restic list blobs' (#5311)
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2025-10-04 12:18:32 +00:00
Michael Eischer b7bbb408ee check: refactor pack selection for read data
Drop the `packs` map from the internal state of the checker. Instead the
Packs(...) method now calls a filter callback that can select the
packs intended for checking.
2025-10-03 23:45:05 +02:00
Michael Eischer 35fca09326 Merge pull request #5489 from MichaelEischer/fix-group-repos
docs: fix permission setup for group-accessible repo
2025-10-03 23:03:50 +02:00
Michael Eischer adbd4a1d18 Fully rework docs for group-accessible repositories
Just tell the user what to do instead of explaining too many details.
I've dropped the read-only variant as it actually has no representation
in the local and sftp backends. Instead it relied on both backends
initially creating all directories, which can't actually be guaranteed.

Based on a suggestion by @brad2014 in significant parts.
2025-10-03 21:24:57 +02:00
Michael Eischer 537d107b6c docs: use absolute permissions for group accessible repositories 2025-10-03 21:24:57 +02:00
Michael Eischer 06aa0f08cb docs: fix permission setup for group-accessible repo
The group always needs execute access for the directories. In addition,
files should be always set to read-only for everyone as restic never
modifies files.
2025-10-03 21:24:57 +02:00
Rani 3ae6a69154 Bugfix(sftp): fix loose permissions on sftp backend. (#5497) 2025-10-03 18:20:52 +00:00
Michael Eischer 264cd67c36 Merge pull request #5532 from MichaelEischer/checker-cleanup
Replace Repository.SetIndex with internal helper
2025-10-03 20:08:14 +02:00
Michael Eischer fd241b8ec7 Merge pull request #5527 from MichaelEischer/drop-s3-static-credentials
s3: drop manual credentials loading from environment
2025-10-03 19:57:55 +02:00
Michael Eischer 76aa9e4f7c Merge pull request #5549 from restic/dependabot/go_modules/github.com/peterbourgon/unixtransport-0.0.7
build(deps): bump github.com/peterbourgon/unixtransport from 0.0.6 to 0.0.7
2025-10-03 19:56:02 +02:00
Michael Eischer aae1acf4d7 check: fix dysfunctional test cases 2025-10-03 19:49:51 +02:00
dependabot[bot] cc0480fc32 build(deps): bump github.com/peterbourgon/unixtransport
Bumps [github.com/peterbourgon/unixtransport](https://github.com/peterbourgon/unixtransport) from 0.0.6 to 0.0.7.
- [Release notes](https://github.com/peterbourgon/unixtransport/releases)
- [Commits](https://github.com/peterbourgon/unixtransport/compare/v0.0.6...v0.0.7)

---
updated-dependencies:
- dependency-name: github.com/peterbourgon/unixtransport
  dependency-version: 0.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-03 17:41:39 +00:00
Michael Eischer 838ef0a9bd Merge pull request #5546 from MichaelEischer/group-golang-dependencies
CI: group dependency updates for golang.org/x/*
2025-10-03 19:40:15 +02:00
Michael Eischer 4426dfe6a9 repository: replace SetIndex method with internal loadIndexWithCallback method 2025-10-03 19:36:57 +02:00
Michael Eischer f0955fa931 repository: add Checker() method to repository to replace unchecked cast 2025-10-03 19:34:33 +02:00
Michael Eischer 189b295c30 repository: add dedicated test helper 2025-10-03 19:34:33 +02:00
Michael Eischer 82971ad7f0 check: split index/pack check into repository package 2025-10-03 19:34:32 +02:00
Michael Eischer bfc2ce97fd check: don't keep extra MasterIndex reference 2025-10-03 19:32:15 +02:00
Michael Eischer d84c3e3c60 CI: group dependency updates for golang.org/x/* 2025-10-03 19:28:30 +02:00
Michael Eischer 93720f0717 Merge pull request #5525 from MichaelEischer/split-restic-directory
Extract snapshot data types from restic package
2025-10-03 19:24:31 +02:00
Michael Eischer 70a24cca85 ignore linter warning 2025-10-03 19:10:40 +02:00
Michael Eischer 56ac8360c7 data: split node and snapshot code from restic package 2025-10-03 19:10:39 +02:00
Michael Eischer c85b157e0e restic: move interfaces between files to prepare refactor 2025-10-03 19:06:32 +02:00
Michael Eischer 13e476e1eb Merge pull request #5518 from MichaelEischer/termstatus-everywhere
Consolidate terminal input/output functionality in termstatus.Terminal
2025-10-03 19:05:28 +02:00
Michael Eischer 3335f62a8f Fix linter warnings 2025-10-03 18:55:46 +02:00
Michael Eischer d8da3d2f2d termstatus: increase test coverage 2025-10-03 18:55:46 +02:00
Michael Eischer df7924f4df node: report error on xattr retrieval using standard error logging 2025-10-03 18:55:46 +02:00
Michael Eischer f2b9ea6455 termstatus: use errWriter if terminal commands fail 2025-10-03 18:55:46 +02:00
Michael Eischer 711194276c remove unused printer from ReadPassword 2025-10-03 18:55:46 +02:00
Michael Eischer f045297348 termstatus: fix typo in comment 2025-10-03 18:55:46 +02:00
Michael Eischer 52eb66929f repository: deduplicate index progress bar initializaton 2025-10-03 18:55:46 +02:00
Michael Eischer b459d66288 termstatus: additional comments 2025-10-03 18:55:46 +02:00
Michael Eischer 76b2cdd4fb replace globalOptions.stdout with termstatus.OutputWriter 2025-10-03 18:55:46 +02:00
Michael Eischer c293736841 drop unused stderr from GlobalOptions 2025-10-03 18:55:46 +02:00
Michael Eischer 1939cff334 restore: embed progress.Printer in restore-specific printer 2025-10-03 18:55:46 +02:00
Michael Eischer 1a76f988ea backup: embed progress.Printer in backup specific printer 2025-10-03 18:55:46 +02:00
Michael Eischer e753941ad3 move NewProgressPrinter to ui package 2025-10-03 18:55:46 +02:00
Michael Eischer ff5a0cc851 termstatus: fully wrap reading password from terminal 2025-10-03 18:55:46 +02:00
Michael Eischer 013c565c29 standardize shorten variable name for GlobalOptions to gopts 2025-10-03 18:55:46 +02:00
Michael Eischer 96af35555a termstatus: add stdin and inject into backup command 2025-10-03 18:55:46 +02:00
Michael Eischer ca5b0c0249 get rid of fmt.Print* usages 2025-10-03 18:55:46 +02:00
Michael Eischer 3410808dcf deduplicate termstatus setup 2025-10-03 18:55:46 +02:00
Michael Eischer 1ae2d08d1b termstatus: centralize OutputIsTerminal checks 2025-10-03 18:55:46 +02:00
Michael Eischer c745e4221e termstatus: use errWriter instead of os.Stderr 2025-10-03 18:22:42 +02:00
Michael Eischer b6c50662da repository: don't ignore cache clearing error 2025-10-03 18:22:42 +02:00
Michael Eischer 4dc71f24c5 backends: pass error logger to backends 2025-10-03 18:22:42 +02:00
Michael Eischer 13f743e26b profiling: inject os.Stderr instead of directly using it 2025-10-03 18:22:42 +02:00
Michael Eischer 3e1632c412 reduce os.stdout / os.stderr usage in tests 2025-10-03 18:22:42 +02:00
Michael Eischer 6bd85d2412 reduce usages of globalOptions variable 2025-10-03 18:22:42 +02:00
Michael Eischer e4395a9d73 Merge pull request #5535 from restic/dependabot/github_actions/docker/login-action-3.6.0
build(deps): bump docker/login-action from 3.5.0 to 3.6.0
2025-10-03 18:21:27 +02:00
Michael Eischer 4d1f6b1fe2 Merge pull request #5536 from restic/dependabot/github_actions/actions/setup-go-6
build(deps): bump actions/setup-go from 5 to 6
2025-10-03 18:20:47 +02:00
Michael Eischer 331260e1d4 Merge pull request #5537 from restic/dependabot/go_modules/github.com/Azure/azure-sdk-for-go/sdk/azidentity-1.12.0
build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity from 1.11.0 to 1.12.0
2025-10-03 18:20:05 +02:00
Michael Eischer eb13789b2b Merge pull request #5528 from MichaelEischer/cleanup-fatalf-usage
Cleanup fatalf usage
2025-10-01 20:17:30 +02:00
dependabot[bot] 0cd079147f build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity
Bumps [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/sdk-breaking-changes-guide-migration.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.11.0...sdk/azcore/v1.12.0)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azidentity
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 01:02:17 +00:00
dependabot[bot] 0b4b092941 build(deps): bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 01:01:54 +00:00
dependabot[bot] 01d3357880 build(deps): bump docker/login-action from 3.5.0 to 3.6.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/184bdaa0721073962dff0199f1fb9940f07167d1...5e57cd118135c172c3672efd75eb46360885c0ef)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 01:01:51 +00:00
Michael Eischer 1c7bb15327 Merge pull request #5451 from greatroar/concurrency
Concurrency simplifications
2025-09-24 22:22:40 +02:00
Michael Eischer d491c1bdbf use errors.Fatalf instead of custom formatting 2025-09-24 22:11:54 +02:00
Michael Eischer 97933d1404 remove trailing newlines from errors.Fatalf calls 2025-09-24 22:11:34 +02:00
Michael Eischer 4edfd36c8f Merge pull request #5363 from zmanda/fix-gh-5258-backup-exits-with-wrong-code-on-ctrl-c
bugfix: fatal errors do not keep underlying error
2025-09-24 22:04:38 +02:00
Michael Eischer a30a36ca51 s3: drop manual credentials loading from environment
credentials.EnvAWS offers a superset of the manually implemented
credentials loading. Rework the error message that is shown if no
credentials were found but either access or secret key are set.
2025-09-24 21:02:02 +02:00
Michael Eischer d52f92e8cc Merge pull request #5523 from tobiaskarch/patch-1
Add OpenContainers labels to Dockerfile.release
2025-09-24 20:37:55 +02:00
Michael Eischer a4e565d921 Merge pull request #5524 from dmotte/pr-fix-backupend
internal/archiver: fixed BackupEnd when SkipIfUnchanged is true
2025-09-24 20:32:44 +02:00
Michael Eischer ec796e6edd Merge pull request #5526 from lyallcooper/patch-1
Fix typo in rewrite command note
2025-09-24 20:31:50 +02:00
Lyall Cooper e30acefbff Fix typo in rewrite command note 2025-09-24 14:40:35 +09:00
Michael Eischer 3e6b5c34c9 Merge pull request #5512 from ProactiveServices/patch-1
doc: mention value for pack size setting
2025-09-23 19:35:38 +02:00
dmotte 9017fefddd internal/archiver: fixed BackupEnd when SkipIfUnchanged is true 2025-09-23 03:07:30 +02:00
Michael Eischer 93d1e3b211 Merge pull request #5519 from MichaelEischer/go-1.25
CI: add go 1.25
2025-09-22 22:44:21 +02:00
Tobias Karch 8f858829ed Add OpenContainers labels to Dockerfile.release 2025-09-22 17:37:17 +00:00
Adam Piggott db3b3e31e6 Line breaks 2025-09-22 14:24:02 +01:00
Michael Eischer 3f7121e180 backup: adapt test to changed error message 2025-09-21 22:59:59 +02:00
Michael Eischer d5dd8ce6a7 CI: add go 1.25 2025-09-21 22:38:34 +02:00
Michael Eischer 08443fe593 Merge pull request #5405 from restic/dependabot/github_actions/golangci/golangci-lint-action-8
build(deps): bump golangci/golangci-lint-action from 6 to 8
2025-09-21 22:37:26 +02:00
Michael Eischer daeb55a4fb Merge pull request #5511 from greatroar/atomic
ui/progress: Restore atomics in Counter
2025-09-21 22:29:40 +02:00
Michael Eischer 6ebc23543d CI: use strict matching for generated source files in golangci-lint 2025-09-21 22:25:57 +02:00
Michael Eischer 7257cd2e5f extra linters 2025-09-21 22:24:35 +02:00
Michael Eischer 88bdf20bd8 Reduce linter ignores 2025-09-21 22:24:27 +02:00
Michael Eischer 8518c1f7d9 CI: convert golangci-lint configuration to v2 2025-09-21 22:24:15 +02:00
Michael Eischer 60d80a6127 Fix linter warnings 2025-09-21 22:24:15 +02:00
Michael Eischer 575eac8d80 CI: bump golangci version to v2 2025-09-21 22:20:37 +02:00
dependabot[bot] 5c667f0501 build(deps): bump golangci/golangci-lint-action from 6 to 8
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6 to 8.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v8)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-21 22:20:37 +02:00
Michael Eischer f091e6aed0 Merge branch 'patch-release' 2025-09-21 21:20:56 +02:00
Alexander Neumann 39a737fe14 Set development version for 0.18.1 2025-09-21 20:05:01 +02:00
Michael Eischer 4871390a81 Merge pull request #5514 from MichaelEischer/term-ui-helper
ui: collect Quote and Truncate helpers
2025-09-21 17:03:56 +02:00
Michael Eischer 65b21e3348 ui: collect Quote and Truncate helpers
Collect ui formatting helpers in the ui package
2025-09-21 16:44:23 +02:00
Michael Eischer 4a7b122fb6 Merge pull request #5510 from MichaelEischer/termstatus-everywhere-print-functions
Replace Printf/Verbosef/Warnf with termstatus
2025-09-21 16:42:29 +02:00
Michael Eischer 86ddee8518 ui: document Message / Printer / Terminal interfaces 2025-09-21 16:32:00 +02:00
Michael Eischer 2fe271980f backup: only pass error log function to helpers 2025-09-21 16:02:59 +02:00
Michael Eischer 4f1390436d init: remove duplication from error message 2025-09-21 15:58:29 +02:00
Michael Eischer 2d7611373e ignore JSON flag for fully unsupported commands
Considering the flag would result in a mostly empty terminal output,
which is probably worse than text output instead of JSON.
2025-09-21 15:38:29 +02:00
Michael Eischer f71278138f drop warnf 2025-09-18 22:58:23 +02:00
Michael Eischer 7d5ebdd0b3 version: convert to termstatus 2025-09-18 22:58:23 +02:00
Michael Eischer d6c75ba2dc prune: drop unused parameter 2025-09-17 21:18:15 +02:00
Michael Eischer 2a9105c050 forget/snapshots: properly change error returned by PrintSnapshots 2025-09-17 21:16:39 +02:00
Adam Piggott 8e87a37df0 doc: mention value for pack size setting 2025-09-16 17:32:26 +01:00
greatroar a8f506ea4d ui/progress: Simplify Updater
Removed a defer'd call that was a bit subtle.
2025-09-16 09:56:33 +02:00
greatroar 0a1ce4f207 ui/progress: Restore atomics in Counter
We switched from atomics to a mutex in #3189 because of an alignment
bug, but the new-style atomic types don't need manual alignment.
2025-09-16 09:49:48 +02:00
Michael Eischer 364271c6c3 Consistently use withTermstatus in tests 2025-09-15 22:37:55 +02:00
Michael Eischer 6b5c8ce14e change run* functions to accept ui.Terminal instead of *termstatus.Terminal 2025-09-15 22:37:25 +02:00
Michael Eischer 5a16b29177 remove unused global output functions 2025-09-15 22:35:48 +02:00
Michael Eischer 320fb5fb98 convert repository open/create to use termstatus 2025-09-15 22:35:32 +02:00
Michael Eischer c14cf48776 further reduce Warnf usages 2025-09-15 22:35:16 +02:00
Michael Eischer 109a211fbe convert repository locking to use termstatus 2025-09-15 22:34:59 +02:00
Michael Eischer 9d3efc2088 cleanup progress bar helpers 2025-09-15 22:34:44 +02:00
Michael Eischer 8b5dbc18ca cleanup progress bar creation special cases 2025-09-15 22:34:28 +02:00
Michael Eischer b0eef4b965 Initialize progress printer as early as reasonable in run functions 2025-09-15 22:34:13 +02:00
Michael Eischer 6c0dccf4a5 self-update: convert to termstatus 2025-09-15 22:33:52 +02:00
Michael Eischer 6b23d0328b find: convert to termstatus 2025-09-15 22:33:41 +02:00
Michael Eischer 52f33d2d54 snapshots: convert to termstatus 2025-09-15 22:19:19 +02:00
Michael Eischer d89535634d unlock: convert to termstatus 2025-09-15 22:19:19 +02:00
Michael Eischer 902cd1e9d6 backup: replace Verbosef usage 2025-09-15 22:19:19 +02:00
Michael Eischer 51299b8ea7 key: convert to termstatus 2025-09-15 22:19:19 +02:00
Michael Eischer fd8f8d64f5 init: convert to termstatus 2025-09-15 22:19:17 +02:00
Michael Eischer 114cc33fe9 generate: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 44dbd4469e tag: replace global print functions with termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer d8f3e35730 prune: replace Print call with termstatus usage 2025-09-15 22:17:26 +02:00
Michael Eischer 333dbd18d8 list: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 0226e46681 cache: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 74fb43e0c2 dump: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 69186350fc diff: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 3e7aad8916 debug: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer c3912ae7bc cat: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer d3e26f2868 ls: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 2e91e81c83 mount: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 0dcd9bee88 rewrite: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer a304826b98 repair snapshots: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 8510f09225 stats: convert to termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer e63aee2ec6 copy: convert to use termstatus 2025-09-15 22:17:26 +02:00
Michael Eischer 94b19d64be termstatus: allow retrieving the underlying writer
This is intended for special cases where it must be guaranteed that the
output on stdout exactly matches what was written to the io.Writer.
2025-09-15 22:17:26 +02:00
Michael Eischer 03600ca509 termstatus: don't buffer stdout
There's not much use in doing so as nearly every write call was paired
with a flush call. Thus, just use an unbuffered writer.
2025-09-15 21:22:07 +02:00
Michael Eischer ef9930cce4 fix capturing stdout with termstatus 2025-09-15 20:25:17 +02:00
Michael Eischer 91ecac8003 termstatus: fix crash when printing empty string 2025-09-15 20:25:17 +02:00
Michael Eischer e9b6149303 list: cleanup parameter order of test helper 2025-09-15 20:25:17 +02:00
Michael Eischer 32b7168a9e centralize index progress bar for termstatus 2025-09-15 20:25:17 +02:00
Michael Eischer 6cdb9a75e6 consider JSON flag in newTerminalProgressPrinter 2025-09-15 20:25:17 +02:00
Michael Eischer 81fe559222 Merge pull request #5495 from MichaelEischer/fix-check-retries
check: fix error reporting on download retry
2025-09-15 19:31:44 +02:00
Michael Eischer f21fd9d115 Merge pull request #5494 from MichaelEischer/fix-background-handling
Refactor terminal background handling
2025-09-13 22:48:11 +02:00
Michael Eischer d757e39992 make linter happy 2025-09-13 22:22:53 +02:00
Srigovind Nayak ce089f7e2d errors: standardize error wrapping for Fatal errors
* replace all occurences of  `errors.Fatal(err.Error())` with `errors.Fatalf("%s", err)` so that the error wrapping is correct across the codebase

* updated the review comments
2025-09-13 23:32:40 +05:30
Srigovind Nayak 576d35b37b changelog: add bugfix changelog for issue-5258 2025-09-13 23:32:40 +05:30
Srigovind Nayak 18b8f8870f tests: add tests for preserving underlying errors 2025-09-13 23:32:39 +05:30
Srigovind Nayak 79c41966af errors: enhance fatalError type to include underlying errors 2025-09-13 23:32:39 +05:30
Michael Eischer c0a30e12b4 extend changelog 2025-09-08 11:54:29 +02:00
Michael Eischer de29d74707 check: fix error reporting on download retry 2025-09-08 11:45:28 +02:00
Michael Eischer 424316e016 extend background handling changelog 2025-09-08 11:04:53 +02:00
Michael Eischer b71b77fa77 terminal: unexport tcgetpgrp, tcsetpgrp and getpgrp 2025-09-08 11:04:38 +02:00
Michael Eischer e7890d7b81 use standard line clearing in printProgress 2025-09-08 11:04:24 +02:00
Michael Eischer 529baf50f8 simplify message printing when restic receives signal 2025-09-08 11:04:11 +02:00
Michael Eischer d10bd1d321 terminal: move reading password from terminal here 2025-09-08 11:03:56 +02:00
Michael Eischer 43b5166de8 terminal: cleanup determining width 2025-09-08 11:03:42 +02:00
Michael Eischer 0b0dd07f15 consolidate checks whether stdin/stdout is terminal 2025-09-08 11:03:26 +02:00
Michael Eischer 93ccc548c8 termstatus: move cursor handling to terminal package 2025-09-08 11:03:17 +02:00
Michael Eischer 0ab38faa2e termstatus: track current status also in background
Without this, restic could temporarily print an outdated status when
moving back into the foreground.
2025-09-08 10:50:53 +02:00
Michael Eischer 48cbbf9651 ui/termstatus: extract background handling code 2025-09-08 10:50:09 +02:00
Michael Eischer 6ff7cd9050 backend/util: extract background handling code 2025-09-08 10:42:35 +02:00
Michael Eischer cc1fe6c111 Merge pull request #5493 from greatroar/ioctl
backend,termstatus: Unify foreground/background detection
2025-09-08 10:40:36 +02:00
greatroar 1ed93bd54d backend,termstatus: Unify foreground/background detection
PR #5358 reintroduced a version of the TIOCGPGRP ioctl call that works
on all Unix platforms except Linux, due to a bug/inconsistency in
x/sys/unix. This commit fixes that by introducing termstatus.Tcgetpgrp.

It also introduces termstatus.Getpgrp and termstatus.Tcsetpgrp to deal
with the different signature of unix.Getpgrp in Solaris vs. all other
Unix platforms and an int-overflowing constant on AIX, so that some
AIX/Solaris-specific code can be removed elsewhere and
foreground/background detection is done the same everywhere except on
Windows.
2025-09-08 09:33:44 +02:00
Michael Eischer c55df65bb6 Merge pull request #5490 from MichaelEischer/sync-compatibility-section
docs: sync compatibility section with website
2025-09-07 11:35:48 +02:00
Michael Eischer 289adebbb7 Merge pull request #5492 from MichaelEischer/update-dependencies
update dependencies
2025-09-07 11:34:37 +02:00
Michael Eischer eef047cfa4 update dependencies 2025-09-06 22:58:30 +02:00
Michael Eischer 7f1ac5764a Merge pull request #5358 from MichaelEischer/fix-tty-capture-in-background
Fix tty capture when started in background
2025-09-06 21:01:03 +02:00
Michael Eischer 52aa1cd17f docs: sync compatibility section with website
This is no change in policy, just a more precise description of the
status quo.
2025-09-06 20:55:48 +02:00
Michael Eischer 42f690dbab add changelog 2025-09-06 20:50:06 +02:00
Michael Eischer 914bd699be backend: always start backend process in separate process group
The process group is necessary to properly handle ctrl-c.
2025-09-06 19:51:10 +02:00
Michael Eischer 4c19d6410f backend: only move child process to foreground if already in foreground
The rclone and sftp backends require starting a child process. The are
first moved into the foreground and back into the background after the
initial startup is done.

However, this behavior is also active if restic itself is started in the
background. In this case, restic changing the foreground process may
confuse the shell and in case of bash causes it to exit. Thus, disable
modification of the controlling process group of the tty if restic is
run in the background.
2025-09-06 19:51:10 +02:00
Michael Eischer 2ad703bfd8 Merge pull request #5486 from greatroar/duration-panic
Avoid panic in duration parsing
2025-09-06 18:32:36 +02:00
greatroar 0864d04c5c internal/restic: Fix panic in ParseDuration
Fixes #5485. Includes test case by @MichaelEischer.
2025-09-06 10:30:34 +02:00
gregosterandGreg Oster 839c38b4c4 EOPNOTSUPP can be returned if the filesystem does not support xattrs (#5344)
---------

Co-authored-by: Greg Oster <oster@netbsd.org>
2025-09-05 19:09:27 +00:00
Michael Eischer e98c44baaf Merge pull request #5421 from MichaelEischer/fix-backup-metadata-crash
backup: fix possible crash if directory disappears before metadata collection
2025-09-05 19:53:16 +02:00
Michael Eischer 01bc60e96f backup: fix test on windows 2025-09-05 19:35:25 +02:00
Michael Eischer 484b706dd8 add changelog 2025-09-05 19:28:18 +02:00
Michael Eischer 350f6452e7 backup: test that parent directory errors can be correctly filtered 2025-09-05 19:28:18 +02:00
Michael Eischer 484cdf12e5 backup: test that missing parent directory is correctly handled 2025-09-05 19:28:18 +02:00
Michael Eischer c8bb7bd312 backup: do not fail backup is some parent folder is inaccessible
Handle errors for parent directories of backup directories in the same
way as all other file access errors during a backup.
2025-09-05 19:28:18 +02:00
Michael Eischer a8ce2e45cc backup: do not crash if nodeFromFileInfo fails
this could crash in two cases:
- if a directory is deleted between restic stating it and trying to list
  its directory content.
- when restic tries to list the parent directory of a backup target, but
  the parent directory has been deleted.

return an error in this case instead.
2025-09-05 19:28:18 +02:00
Michael Eischer 275507fb3e Merge pull request #5452 from dominikschulz/fix/storage
Mark HTTP Error 507 as permanent
2025-09-05 19:17:15 +02:00
Michael Eischer 391c27975a Merge pull request #5460 from restic/dependabot/go_modules/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob-1.6.2
build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob from 1.6.1 to 1.6.2
2025-09-05 19:16:54 +02:00
Michael Eischer 7d47e60e27 Merge pull request #5481 from restic/dependabot/github_actions/actions/checkout-5
build(deps): bump actions/checkout from 4 to 5
2025-09-05 19:07:58 +02:00
Michael Eischer 5c17c277f3 add changelog 2025-09-05 19:07:09 +02:00
Dominik Schulz 48e5c0984e Mark HTTP Error 507 as permanent
This change classifies HTTP error 507 (Insufficient Storage) as a
permanent error that should not be retried. I keep running into
this once in a while and there is literally no point in retrying when
the server is full.

Fixes #5429

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
2025-09-05 19:04:16 +02:00
dependabot[bot] 2414771a59 build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
Bumps [github.com/Azure/azure-sdk-for-go/sdk/storage/azblob](https://github.com/Azure/azure-sdk-for-go) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/go-mgmt-sdk-release-guideline.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.6.1...sdk/storage/azblob/v1.6.2)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
  dependency-version: 1.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 16:58:21 +00:00
Michael Eischer 0e381bdbf1 Merge pull request #5483 from restic/dependabot/go_modules/google.golang.org/api-0.248.0
build(deps): bump google.golang.org/api from 0.228.0 to 0.248.0
2025-09-05 18:47:58 +02:00
dependabot[bot] 11cd4a0a88 build(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 16:38:46 +00:00
dependabot[bot] f54989f634 build(deps): bump google.golang.org/api from 0.228.0 to 0.248.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.228.0 to 0.248.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.228.0...v0.248.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-version: 0.248.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 16:36:17 +00:00
Michael Eischer af7f10d16b Merge pull request #5482 from restic/dependabot/github_actions/docker/login-action-3.5.0
build(deps): bump docker/login-action from 3.4.0 to 3.5.0
2025-09-05 18:35:59 +02:00
Michael Eischer 6de95a3d58 Merge pull request #5484 from restic/dependabot/go_modules/golang.org/x/crypto-0.41.0
build(deps): bump golang.org/x/crypto from 0.39.0 to 0.41.0
2025-09-05 18:35:24 +02:00
dependabot[bot] 93f436e999 build(deps): bump golang.org/x/crypto from 0.39.0 to 0.41.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.39.0 to 0.41.0.
- [Commits](https://github.com/golang/crypto/compare/v0.39.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 02:10:03 +00:00
dependabot[bot] 6edb199efd build(deps): bump docker/login-action from 3.4.0 to 3.5.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/74a5d142397b4f367a81961eba4e8cd7edddf772...184bdaa0721073962dff0199f1fb9940f07167d1)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 01:57:28 +00:00
rawtaz 9b2c0a0c54 Merge pull request #5466 from y0n3d4/gentoo-install-info
Added Gentoo install info
2025-08-07 13:28:17 +02:00
y0n3d4 64273ea027 Update 020_installation.rst removing command options
Removed command options: their use is a user choice
2025-08-06 13:57:29 +02:00
Michele Testa 5a00d26431 Update 020_installation.rst adding instruction for Gentoo Linux 2025-08-05 15:16:37 +02:00
Michele Testa 3faad5751d Revert "Update 020_installation.rst adding install command for Gentoo Linux"
This reverts commit f487eb1c66.
2025-08-05 14:12:04 +02:00
y0n3d4 f487eb1c66 Update 020_installation.rst adding install command for Gentoo Linux
Added basic instructions for restic installation on Gentoo using the official package manager (Portage)
2025-08-05 14:01:10 +02:00
Michael Eischer 72636238d0 Merge pull request #5400 from rhhub/patch-2
docs: clarify ** wildcard must me between path separators
2025-08-04 20:56:07 +02:00
rawtaz 51098157e2 Merge pull request #5462 from TheAlchemistOf42/wasabi-link
Correct Wasabi link
2025-08-02 22:12:57 +02:00
A Crutcher 0b080c44d7 doc: Correct Wasabi link 2025-08-02 13:03:43 -05:00
Michael Eischer b71fe91643 Merge pull request #5345 from mikix/chmod-enotsup
backend/local: ignore chmod "not supported" errors
2025-07-21 22:28:53 +02:00
Michael Eischer 9c3b8d171a Merge pull request #5434 from restic/dependabot/go_modules/golang.org/x/time-0.12.0
build(deps): bump golang.org/x/time from 0.11.0 to 0.12.0
2025-07-21 22:17:40 +02:00
Michael Eischer ddb7fb837b Merge pull request #5435 from restic/dependabot/go_modules/github.com/Azure/azure-sdk-for-go/sdk/azidentity-1.10.1
build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity from 1.10.0 to 1.10.1
2025-07-21 22:14:29 +02:00
Michael Eischer 3433c5abac Merge pull request #5408 from MichaelEischer/fix-walker-crash
walker: fix error handling if tree cannot be loaded
2025-07-21 21:46:59 +02:00
dependabot[bot] 09bc58c950 build(deps): bump golang.org/x/time from 0.11.0 to 0.12.0
Bumps [golang.org/x/time](https://github.com/golang/time) from 0.11.0 to 0.12.0.
- [Commits](https://github.com/golang/time/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: golang.org/x/time
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 19:46:35 +00:00
Michael Eischer 20eb9018a0 Merge pull request #5409 from MichaelEischer/fix-release-notes-typos
Fix release note typos
2025-07-21 21:46:05 +02:00
Michael Eischer 651f553530 Merge pull request #5436 from restic/dependabot/go_modules/github.com/peterbourgon/unixtransport-0.0.6
build(deps): bump github.com/peterbourgon/unixtransport from 0.0.4 to 0.0.6
2025-07-21 21:45:06 +02:00
Michael Eischer aad4b53ead Merge pull request #5438 from restic/dependabot/go_modules/golang.org/x/crypto-0.39.0
build(deps): bump golang.org/x/crypto from 0.38.0 to 0.39.0
2025-07-21 21:42:06 +02:00
greatroar f7f6459eb9 internal/restic: Simplify ParallelRemove 2025-07-19 12:55:40 +02:00
greatroar 95a36b55f4 internal/dump: Clarify writeNode concurrency 2025-07-19 12:54:41 +02:00
greatroar 2c39b1f84f internal/repository/index: Simplify MasterIndex concurrency 2025-07-18 15:06:37 +02:00
dependabot[bot] e467496ace build(deps): bump golang.org/x/crypto from 0.38.0 to 0.39.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/crypto/compare/v0.38.0...v0.39.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 05:46:21 +00:00
dependabot[bot] f2de260524 build(deps): bump github.com/peterbourgon/unixtransport
Bumps [github.com/peterbourgon/unixtransport](https://github.com/peterbourgon/unixtransport) from 0.0.4 to 0.0.6.
- [Release notes](https://github.com/peterbourgon/unixtransport/releases)
- [Commits](https://github.com/peterbourgon/unixtransport/compare/v0.0.4...v0.0.6)

---
updated-dependencies:
- dependency-name: github.com/peterbourgon/unixtransport
  dependency-version: 0.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 05:07:50 +00:00
dependabot[bot] c17d5ab2e1 build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity
Bumps [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/go-mgmt-sdk-release-guideline.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.10.0...sdk/azidentity/v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azidentity
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 05:07:41 +00:00
Michael Terry a8535aba58 backend/local: ignore chmod "not supported" errors 2025-06-02 17:44:30 -04:00
rhhub 521fbad701 docs: clarify ** must me between path separators 2025-06-02 13:46:06 -07:00
Michael Eischer 15b7d7c3fc Fix release note typos 2025-06-02 21:12:37 +02:00
Patrick Wolf 7d39b1bfe8 Update 047_tuning_backup_parameters.rst - local backend (#5355)
users would find it helpful to know how to adjust the "local" backend and they might not get the idea that the local backend is just called local... which in turn leads them to think restic is slow as they can't adjust away  from 2 threads for restore and backup.
2025-06-02 18:40:04 +00:00
Michael Eischer e4a7f4aadf Merge pull request #5356 from MichaelEischer/fix-backup-stdin-filename
backup: Fix `--stdin-filename` with directory
2025-06-02 20:27:26 +02:00
Michael Eischer 10cfe96cd4 walker: fix error handling if tree cannot be loaded
A tree that cannot be loaded is a fatal error when walking the tree.
Thus, return the error and exit the tree walk.
2025-06-02 20:04:26 +02:00
Michael Eischer 2eaa79d33f Merge pull request #5374 from ilyagr/docprofile
docs: document profiling options a bit better
2025-06-02 19:57:33 +02:00
Ilya GrigorievandMichael Eischer 99ee5696f3 bugfix: have --{cpu,mem,...}-profile work even if Restic exits with error code (#5373)
* bugfix: write pprof file for `--{cpu,mem,...}-profile` even on error code

Before this, if `restic backup --cpu-profile dir/ backup-dir/` couldn't
read some of the input files (e.g. they weren't readable by the user
restic was running under), the `cpu.pprof` file it outputs would be
empty.

https://github.com/spf13/cobra/issues/1893

* drop changelog as it's not relevant for end users

---------

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2025-06-02 17:57:07 +00:00
Ilya Grigoriev e8dbb69a94 docs: when describing profiling, briefly explain .pprof files 2025-06-02 19:49:19 +02:00
Ilya Grigoriev f4e21cdb75 docs: document profiling options a bit better
Previously, the docs were a bit mysterious about what "enables profiling
support" means or how one could take advantage of it.
2025-06-02 19:49:19 +02:00
Michael Eischer e5bdc3c74f Merge pull request #5382 from restic/dependabot/go_modules/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob-1.6.1
build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob from 1.6.0 to 1.6.1
2025-06-02 19:40:33 +02:00
Michael Eischer 7e51c928c4 Merge pull request #5384 from zmanda/feat-gh-5377-check-add-percentage-for-read-data-subset
check: add percentage of repository checked
2025-06-02 19:37:08 +02:00
dependabot[bot] 21e87851aa build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
Bumps [github.com/Azure/azure-sdk-for-go/sdk/storage/azblob](https://github.com/Azure/azure-sdk-for-go) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.6.0...sdk/azcore/v1.6.1)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
  dependency-version: 1.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 17:32:18 +00:00
Michael Eischer 2bc1bf2702 Merge pull request #5402 from restic/dependabot/go_modules/github.com/Azure/azure-sdk-for-go/sdk/azidentity-1.10.0
build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity from 1.8.2 to 1.10.0
2025-06-02 19:19:10 +02:00
dependabot[bot] df110060d1 build(deps): bump github.com/Azure/azure-sdk-for-go/sdk/azidentity
Bumps [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) from 1.8.2 to 1.10.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/go-mgmt-sdk-release-guideline.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azidentity/v1.8.2...sdk/azcore/v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azidentity
  dependency-version: 1.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 16:45:38 +00:00
Michael Eischer 337a7d1205 Merge pull request #5385 from Silvenga/windows-docs
docs: updated installation docs for Windows
2025-06-02 18:44:43 +02:00
Michael Eischer 322e271dd2 Merge pull request #5404 from restic/dependabot/go_modules/golang.org/x/sys-0.33.0
build(deps): bump golang.org/x/sys from 0.31.0 to 0.33.0
2025-06-02 18:35:34 +02:00
dependabot[bot] 1ac224458f build(deps): bump golang.org/x/sys from 0.31.0 to 0.33.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.31.0 to 0.33.0.
- [Commits](https://github.com/golang/sys/compare/v0.31.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 01:25:20 +00:00
rawtaz 126ad04568 Merge pull request #5389 from fronesis47/patch-3
Update passwords section of FAQ
2025-05-11 20:35:20 +02:00
Samuel Chambers e732bdbfb8 updated doc/faq.rst_commitsSquashed 2025-05-11 14:02:07 -04:00
Mark Lopez 2db08fd749 docs: updated installation docs for Windows 2025-05-03 10:27:50 -05:00
Srigovind Nayak debb110a7c check: add percentage of repository checked 2025-05-03 18:08:12 +05:30
rawtaz 5eb4f5af61 Merge pull request #5360 from makuhama/master
doc: typo & minor rewording in 'Removing files from snapshots'
2025-05-02 01:05:08 +02:00
Markus Hansmair 287b601f01 doc: typo & minor rewording in 'Removing files from snapshots' 2025-04-16 09:37:26 +02:00
Michael Eischer 64c82a5d9c Merge pull request #5357 from Hello71/patch-1
doc: add fastest, better compression
2025-04-14 20:02:41 +02:00
Alex Xu 12f36ebf07 doc: add fastest, better compression
Follow-up for #5321
2025-04-13 19:33:13 -04:00
Michael Eischer 45e09dca2a add changelog for --stdin-filename with/directory 2025-04-11 22:29:18 +02:00
Michael Eischer 5bb9d0d996 backup: test subdirectories in stdin filenames work 2025-04-11 22:14:32 +02:00
Michael Eischer 9f39e8a1d3 fs/reader: return proper error on invalid filename 2025-04-11 22:07:31 +02:00
Michael Eischer ddd48f1e98 fs/reader: test file not exist case 2025-04-11 21:57:45 +02:00
Michael Eischer 6e91ea3397 fs/reader: use test helpers 2025-04-11 21:54:15 +02:00
Michael Eischer e7c1e4f1ff fs/reader: deduplicate test code 2025-04-11 21:50:47 +02:00
Michael Eischer 70e1037a49 fs/reader: fix open+stat handling 2025-04-11 21:49:25 +02:00
Michael Eischer 19f48084ea fs/reader: use modification time for file and directories
This ensures that a fixed input generates a fully deterministic output
file structure.
2025-04-11 21:46:24 +02:00
Michael Eischer 3a995172b7 fs: rewrite Reader to build fs tree up front
This adds proper support for filenames that include directories. For
example, `/foo/bar` would result in an error when trying to open `/foo`.

The directory tree is now build upfront. This ensures let's the
directory tree construction be handled only once. All accessors then
only have to look up the constructed directory entries.
2025-04-11 21:37:40 +02:00
Michael Eischer 0dffa1208d Merge pull request #5243 from zmanda/feat-gh-4868-show-repo-id-in-df-and-mount
mount: append the repository ID to the name of the FUSE mount
2025-04-02 21:16:11 +02:00
Michael Eischer 6fbcce1d1a docs: fix typos in developer information (#5329) 2025-04-02 21:12:43 +02:00
Michael Eischer e8d458be7e update direct dependencies (#5340) 2025-04-02 21:10:40 +02:00
dependabot[bot] 4471c7847b build(deps): bump docker/login-action from 3.3.0 to 3.4.0 (#5333)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/9780b0c442fbb1117ed29e0efdff1e18412f7567...74a5d142397b4f367a81961eba4e8cd7edddf772)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-02 20:54:17 +02:00
Srigovind Nayak f13e9c10a4 Add support for additional compression levels fastest and better (#5321)
* repository: expose addtional compression levels

* adding better and fastest compression levels for zstd

* repository: add changelog entry for issue-4728

* chore: fix golint issues

* chore: sort compression modes in the help text

* updating review comments
2025-03-31 21:21:12 +02:00
Michael Eischer f768683162 Merge pull request #5322 from zmanda/fix-gh-5233-forget-failure-exit-codes
forget: return exit code 3 on partial removal of snapshots
2025-03-31 20:08:33 +02:00
Michael Eischer 0b7bdfed7e Merge pull request #5320 from mjnaderi/patch-1
doc: fix typos
2025-03-31 20:07:51 +02:00
Michael Eischer a4fe94ec82 Merge pull request #5317 from gilbsgilbs/fix-s3-restore-timeout-unit
docs: fix unit for S3 restore timeout
2025-03-31 20:01:22 +02:00
Michael Eischer 6684d1d2f5 Merge pull request #5327 from MichaelEischer/fix-forget-hostname-default
forget: fix ignored RESTIC_HOST environment variable
2025-03-31 19:21:13 +02:00
Michael Eischer e1f7522174 forget: fix ignored RESTIC_HOST environment variable 2025-03-31 18:10:17 +02:00
Srigovind Nayak d1649affb2 chore: update changelog for issue-5233 2025-03-30 14:12:06 +05:30
Srigovind Nayak 936c783c0f forget: exit code 3 for snapshot removal failures 2025-03-30 14:11:32 +05:30
Mohammad Javad Naderi 5614cf4758 doc: fix typos 2025-03-29 12:40:47 +03:30
Srigovind Nayak 6db0d84ab0 changelog: add changelog entry for issue-4868 2025-03-29 13:27:50 +05:30
Srigovind Nayak 88b599c4f3 mount: append repository ID to FS name of FUSE mount
* update review comments

mount: append repository ID to FS name of the FUSE mount
2025-03-29 13:22:10 +05:30
Gilbert Gilb's eefff0d793 docs: fix unit for S3 restore timeout
"d" is not a valid unit.
2025-03-27 21:20:50 +01:00
Alexander Neumann 3d14e92905 Set development version for 0.18.0 2025-03-27 20:17:36 +01:00
546 changed files with 15082 additions and 8095 deletions
+25
View File
@@ -0,0 +1,25 @@
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
# Skip historical content (CHANGELOG, release notes archive), auto-generated
# man pages (which contain troff macros like \(bu that look like typos), and
# files we don't want to modify (go.sum, css with vendor prefixes).
skip = .git*,.gitignore,.gitattributes,go.sum,*.css,.codespellrc,CHANGELOG.md,changelog,*.1
check-hidden = true
# Protect URLs from corrections (URLs may contain words flagged as typos and
# must not be changed); also ignore camelCase/PascalCase identifiers like
# AtLeast which are valid Go identifiers, not typos.
ignore-regex = https?://\S+|\b[a-z]+[A-Z]\w*\b|\b[A-Z][a-z]+[A-Z]\w*\b
ignore-words-list =
# variable name short for "serialized", also matches [uU]ser glob pattern
ser,
# German word "ist" appearing in test data string
ist,
# intentional truncated test string in format truncation tests
fo,
# CLI flag name (--iinclude, case-insensitive include)
iinclude,
# intentional in fake test path /doesnt/exist
doesnt
+12
View File
@@ -0,0 +1,12 @@
# Actual layer caching is impossible due to .git, but
# that must be included for provenance reasons. These ignores
# are strictly for hygienic build.
*
!/*.go
!/go.*
!/cmd/*
!/docker/entrypoint.sh
!/internal/*
!/helpers/*
!/VERSION
!/.git/
+1 -1
View File
@@ -36,7 +36,7 @@ Please always follow these steps:
- Format all commit messages in the same style as [the other commits in the repository](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits). - Format all commit messages in the same style as [the other commits in the repository](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
--> -->
- [ ] I have added tests for all code changes. - [ ] I have added tests for all code changes, see [writing tests](https://restic.readthedocs.io/en/stable/090_participating.html#writing-tests)
- [ ] I have added documentation for relevant changes (in the manual). - [ ] I have added documentation for relevant changes (in the manual).
- [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/restic/blob/master/changelog/TEMPLATE)). - [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/restic/blob/master/changelog/TEMPLATE)).
- [ ] I'm done! This pull request is ready for review. - [ ] I'm done! This pull request is ready for review.
+4
View File
@@ -5,6 +5,10 @@ updates:
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "monthly" interval: "monthly"
groups:
golang-x-deps:
patterns:
- "golang.org/x/*"
# Dependencies listed in .github/workflows/*.yml # Dependencies listed in .github/workflows/*.yml
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
+27
View File
@@ -0,0 +1,27 @@
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
# run checks on push to master, but not when other branches are pushed to
push:
branches:
- master
# run checks for all pull requests
pull_request:
merge_group:
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Codespell
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
+6 -6
View File
@@ -26,10 +26,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -37,7 +37,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: | tags: |
@@ -47,10 +47,10 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
- name: Ensure consistent binaries - name: Ensure consistent binaries
run: | run: |
@@ -61,7 +61,7 @@ jobs:
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
with: with:
push: true push: true
context: . context: .
+21 -19
View File
@@ -13,7 +13,7 @@ permissions:
contents: read contents: read
env: env:
latest_go: "1.24.x" latest_go: "1.26.x"
GO111MODULE: on GO111MODULE: on
jobs: jobs:
@@ -23,29 +23,29 @@ jobs:
# list of jobs to run: # list of jobs to run:
include: include:
- job_name: Windows - job_name: Windows
go: 1.24.x go: 1.26.x
os: windows-latest os: windows-latest
- job_name: macOS - job_name: macOS
go: 1.24.x go: 1.26.x
os: macOS-latest os: macOS-latest
test_fuse: false test_fuse: false
- job_name: Linux - job_name: Linux
go: 1.24.x go: 1.26.x
os: ubuntu-latest os: ubuntu-latest
test_cloud_backends: true test_cloud_backends: true
test_fuse: true test_fuse: true
check_changelog: true check_changelog: true
- job_name: Linux (race) - job_name: Linux (race)
go: 1.24.x go: 1.26.x
os: ubuntu-latest os: ubuntu-latest
test_fuse: true test_fuse: true
test_opts: "-race" test_opts: "-race"
- job_name: Linux - job_name: Linux
go: 1.23.x go: 1.25.x
os: ubuntu-latest os: ubuntu-latest
test_fuse: true test_fuse: true
@@ -57,10 +57,10 @@ jobs:
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Set up Go ${{ matrix.go }} - name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
@@ -137,12 +137,14 @@ jobs:
go run build.go go run build.go
- name: Minimal test - name: Minimal test
timeout-minutes: 3
run: | run: |
./restic init ./restic init
./restic backup . ./restic backup .
env: env:
RESTIC_REPOSITORY: ../testrepo RESTIC_REPOSITORY: ../testrepo
RESTIC_PASSWORD: password RESTIC_PASSWORD: password
RESTIC_DEBUG_STACKTRACE_SIGINT: "true"
- name: Run local Tests - name: Run local Tests
env: env:
@@ -220,10 +222,10 @@ jobs:
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Set up Go ${{ env.latest_go }} - name: Set up Go ${{ env.latest_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: ${{ env.latest_go }} go-version: ${{ env.latest_go }}
@@ -242,18 +244,18 @@ jobs:
checks: write checks: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Set up Go ${{ env.latest_go }} - name: Set up Go ${{ env.latest_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: ${{ env.latest_go }} go-version: ${{ env.latest_go }}
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v9
with: with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.64.8 version: v2.12
args: --verbose --timeout 5m args: --verbose --timeout 5m
# only run golangci-lint for pull requests, otherwise ALL hints get # only run golangci-lint for pull requests, otherwise ALL hints get
@@ -287,11 +289,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
# list of Docker images to use as base name for tags # list of Docker images to use as base name for tags
images: | images: |
@@ -307,14 +309,14 @@ jobs:
type=sha type=sha
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
push: false push: false
context: . context: .
+81 -56
View File
@@ -1,70 +1,95 @@
# This is the configuration for golangci-lint for the restic project. version: "2"
#
# A sample config with all settings is here:
# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
linters: linters:
# only enable the linters listed below # only enable the linters listed below
disable-all: true default: none
enable: enable:
- asciicheck
# ensure that http response bodies are closed
- bodyclose
# restrict imports from other restic packages for internal/backend (cache exempt)
- depguard
- copyloopvar
# make sure all errors returned by functions are handled # make sure all errors returned by functions are handled
- errcheck - errcheck
# show how code can be simplified
- gosimple
# make sure code is formatted
- gofmt
# examine code and report suspicious constructs, such as Printf calls whose # examine code and report suspicious constructs, such as Printf calls whose
# arguments do not align with the format string # arguments do not align with the format string
- govet - govet
# consistent imports
# make sure names and comments are used according to the conventions - importas
- revive
# detect when assignments to existing variables are not used # detect when assignments to existing variables are not used
- ineffassign - ineffassign
- nolintlint
# make sure names and comments are used according to the conventions
- revive
# run static analysis and find errors # run static analysis and find errors
- staticcheck - staticcheck
# find unused variables, functions, structs, types, etc. # find unused variables, functions, structs, types, etc.
- unused - unused
settings:
# parse and typecheck code depguard:
- typecheck rules:
# Prevent backend packages from importing the internal/restic package to keep the architectural layers intact.
# ensure that http response bodies are closed backend-imports:
- bodyclose files:
- "**/internal/backend/**"
- importas - "!**/internal/backend/cache/**"
- "!**/internal/backend/test/**"
issues: - "!**/*_test.go"
# don't use the default exclude rules, this hides (among others) ignored deny:
# errors from Close() calls - pkg: "github.com/restic/restic/internal/restic"
exclude-use-default: false desc: "internal/restic should not be imported to keep the architectural layers intact"
- pkg: "github.com/restic/restic/internal/repository"
# list of things to not warn about desc: "internal/repository should not be imported to keep the architectural layers intact"
exclude: importas:
# revive: do not warn about missing comments for exported stuff alias:
- exported (function|method|var|type|const) .* should have comment or be unexported - pkg: github.com/restic/restic/internal/test
# revive: ignore constants in all caps alias: rtest
- don't use ALL_CAPS in Go names; use CamelCase staticcheck:
# revive: lots of packages don't have such a comment checks:
- "package-comments: should have a package comment" # default
# staticcheck: there's no easy way to replace these packages - "all"
- "SA1019: \"golang.org/x/crypto/poly1305\" is deprecated" - "-ST1000"
- "SA1019: \"golang.org/x/crypto/openpgp\" is deprecated" - "-ST1003"
- "redefines-builtin-id:" - "-ST1016"
- "-ST1020"
exclude-rules: - "-ST1021"
# revive: ignore unused parameters in tests - "-ST1022"
- path: (_test\.go|testing\.go|backend/.*/tests\.go) # extra disables
text: "unused-parameter:" - "-QF1008" # don't warn about specifying name of embedded field on access
exclusions:
linters-settings: rules:
importas: # revive: ignore unused parameters in tests
alias: - path: (_test\.go|testing\.go|backend/.*/tests\.go)
- pkg: github.com/restic/restic/internal/test text: "unused-parameter:"
alias: rtest # revive: do not warn about missing comments for exported stuff
- path: (.+)\.go$
text: exported (function|method|var|type|const) .* should have comment or be unexported
# revive: ignore constants in all caps
- path: (.+)\.go$
text: don't use ALL_CAPS in Go names; use CamelCase
# revive: lots of packages don't have such a comment
- path: (.+)\.go$
text: "package-comments: should have a package comment"
# staticcheck: there's no easy way to replace these packages
- path: (.+)\.go$
text: 'SA1019: "golang.org/x/crypto/poly1305" is deprecated'
- path: (.+)\.go$
text: 'SA1019: "golang.org/x/crypto/openpgp" is deprecated'
- path: (.+)\.go$
text: "redefines-builtin-id:"
# revive: collection of helpers to implement a backend, more descriptive names would be too repetitive
- path: internal/backend/util/.*.go$
text: "var-naming: avoid meaningless package names"
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
# make sure code is formatted
- gofmt
exclusions:
paths:
- third_party$
- builtin$
- examples$
+626 -2
View File
@@ -1,5 +1,7 @@
# Table of Contents # Table of Contents
* [Changelog for 0.19.1](#changelog-for-restic-0191-2026-07-05)
* [Changelog for 0.19.0](#changelog-for-restic-0190-2026-06-09)
* [Changelog for 0.18.1](#changelog-for-restic-0181-2025-09-21) * [Changelog for 0.18.1](#changelog-for-restic-0181-2025-09-21)
* [Changelog for 0.18.0](#changelog-for-restic-0180-2025-03-27) * [Changelog for 0.18.0](#changelog-for-restic-0180-2025-03-27)
* [Changelog for 0.17.3](#changelog-for-restic-0173-2024-11-08) * [Changelog for 0.17.3](#changelog-for-restic-0173-2024-11-08)
@@ -40,6 +42,627 @@
* [Changelog for 0.6.0](#changelog-for-restic-060-2017-05-29) * [Changelog for 0.6.0](#changelog-for-restic-060-2017-05-29)
# Changelog for restic 0.19.1 (2026-07-05)
The following sections list the changes in restic 0.19.1 relevant to
restic users. The changes are ordered by importance.
## Summary
* Fix #5234: Prevent mounting over the repository directory
* Fix #5667: Skip inaccessible `backup` source paths
* Fix #5722: Update `mount` latest symlink after snapshot reload
* Fix #21866: Hide `stats` progress bar in JSON mode
* Fix #21869: Restore old behavior of `snapshots --latest <n>` without `--group-by`
* Fix #21876: Show timezone location in `snapshots` output
* Fix #21879: Prevent crash in mountpoint validation if mountpoint is inaccessible
* Fix #21895: Remove read-only files via the SFTP backend on Windows servers
* Fix #21899: Make `backup` respect excludes for duplicate directory entries
## Details
* Bugfix #5234: Prevent mounting over the repository directory
Using a local repository directory as the `mount` target — or a path that
contains it, or that it contains — caused the FUSE server to read its own
backend files through the new mount, deadlocking the kernel and requiring a long
reboot to recover.
Restic now resolves both paths and refuses any such overlap with a clear error
before mounting.
https://github.com/restic/restic/issues/5234
https://github.com/restic/restic/pull/5348
* Bugfix #5667: Skip inaccessible `backup` source paths
The `backup` command only skipped source paths that did not exist. A path that
could not be accessed for another reason, such as a malformed path on Windows,
was kept and produced an empty snapshot. Restic now skips any such path and
aborts if none remain.
https://github.com/restic/restic/issues/5667
https://github.com/restic/restic/pull/21852
* Bugfix #5722: Update `mount` latest symlink after snapshot reload
When `restic mount` was kept running while new snapshots were created, the new
snapshots appeared in the mountpoint, but the `latest` symlink could still point
to the previously latest snapshot. Restic now invalidates the cached snapshot
directory entries after a snapshot reload so that `latest` points to the newest
snapshot.
https://github.com/restic/restic/issues/5722
https://github.com/restic/restic/pull/21873
* Bugfix #21866: Hide `stats` progress bar in JSON mode
Since restic 0.19.0, the `stats` command shows a progress bar. This progress bar
was unintentionally displayed also when using the `--json` option, mixing
regular text output with JSON. This is now fixed.
https://github.com/restic/restic/issues/21866
https://github.com/restic/restic/pull/21871
* Bugfix #21869: Restore old behavior of `snapshots --latest <n>` without `--group-by`
Restic 0.19.0 accidentally changed the behavior of `snapshots --latest <n>` to
no longer group snapshots by host and paths by default.
The `snapshots --latest <n>` command now again uses the old behavior of grouping
by host and paths when `--group-by` is not specified. However, when specifying
`--group-by` the output is still grouped as requested, as in restic 0.19.0.
https://github.com/restic/restic/issues/21869
https://github.com/restic/restic/pull/21875
* Bugfix #21876: Show timezone location in `snapshots` output
With restic 0.19.0, the `snapshots` command printed the current timezone when
listing snapshots. However, that timezone label might change during the year,
for example with daylight saving time. Restic now prints a more consistent and
shorter version of the text.
https://github.com/restic/restic/pull/21876
https://forum.restic.net/t/possible-bug-in-timezone-naming/10867
* Bugfix #21879: Prevent crash in mountpoint validation if mountpoint is inaccessible
Since restic 0.19.0, the `mount` command validates a mountpoint before loading
the repository. If restic was unable to stat the mountpoint, this would result
in a crash. This has now been fixed to correctly return an error instead.
https://github.com/restic/restic/pull/21879
* Bugfix #21895: Remove read-only files via the SFTP backend on Windows servers
Since restic 0.19.0, repository files on the SFTP backend are marked read-only
after save. On Windows SFTP servers, removing them failed with a permission
error. The SFTP backend now clears the read-only flag before removing the file.
https://github.com/restic/restic/issues/21895
https://github.com/restic/restic/pull/21897
* Bugfix #21899: Make `backup` respect excludes for duplicate directory entries
Since restic 0.19.0, backing up a directory with duplicate directory entries
always produced "Warning: at least one source file could not be read", even when
those files were excluded. This has now been fixed.
https://github.com/restic/restic/issues/21899
https://github.com/restic/restic/pull/21900
# Changelog for restic 0.19.0 (2026-06-09)
The following sections list the changes in restic 0.19.0 relevant to
restic users. The changes are ordered by importance.
## Summary
* Fix #2034: Support serving a `restic mount` of a Windows system via Samba
* Fix #4447: Use mode 0700 for repository directories created over SFTP
* Fix #4467: Exit with code 3 when some `backup` source paths do not exist
* Fix #4759: Error out when environment variables hold invalid values
* Fix #5233: Return exit code 3 when failing to remove snapshots
* Fix #5258: Exit with code 130 on SIGINT
* Fix #5280: Reject impossible `find` time bounds immediately
* Fix #5280: Make `find --pack` list blobs for tree packs
* Fix #5354: Allow `rclone` and `sftp` backends when running in background
* Fix #5427: Correctly restore ACL inheritance state on Windows
* Fix #5477: Password prompt was sometimes not shown for `backup -v`
* Fix #5487: Mark repository files read-only when using the SFTP backend
* Fix #5586: Correctly handle `snapshots --group-by` with `--latest`
* Fix #5595: Avoid spurious `chmod` errors on certain file backends
* Fix #5683: Prevent `backup --stdin-from-command` from hanging
* Fix #5757: Respect `--user` and `--host` in `key passwd`
* Fix #21820: Correct handling of duplicate index entries
* Fix #21820: Correctly handle pack files missing from the index
* Chg #5293: Prune small packfiles more aggressively
* Chg #5767: Prevent excluding paths explicitly passed to `backup`
* Chg #21791: Update dependencies and require Go 1.25 or newer
* Enh #3326: Limit `check` to snapshots selected by filters
* Enh #3572: Support restoring ownership by name on UNIX systems
* Enh #3738: Optional GitHub token for `self-update` API requests
* Enh #4278: Support include filters in the `rewrite` command
* Enh #4728: Support zstd compression levels `fastest` and `better`
* Enh #4868: Include repository ID in the filesystem name used by `mount`
* Enh #5175: Add status counters to `copy` in verbose text output
* Enh #5352: Support excluding cloud-backed files on macOS
* Enh #5383: Reduce progress bar refresh rates to decrease energy usage
* Enh #5424: Enable Windows filesystem privileges before file access
* Enh #5440: Make `--host` override environment variable `RESTIC_HOST`
* Enh #5448: Support configuring `nice` and `ionice` in the Docker image
* Enh #5453: Copy multiple snapshots in batches
* Enh #5523: Add Open Container Initiative labels to release Docker image
* Enh #5531: Reduce Azure storage costs by optimizing uploads
* Enh #5562: Rewrite only changed status lines each frame
* Enh #5588: Show timezone context in `snapshots` output
* Enh #5610: Reduce `check`, `copy`, `diff` and `stats` memory usage
* Enh #5689: Show more detailed progress for `stats`
* Enh #5713: Significantly speed up index loading
* Enh #5718: Stricter and earlier validation of the `mount` point
## Details
* Bugfix #2034: Support serving a `restic mount` of a Windows system via Samba
A repository mounted using `restic mount` on a POSIX system could not use Samba
to serve files from backups of Windows systems, while backups of non-Windows
systems could be served successfully. This has now been fixed.
https://github.com/restic/restic/issues/2034
https://github.com/restic/restic/issues/4382
https://github.com/restic/restic/pull/21784
* Bugfix #4447: Use mode 0700 for repository directories created over SFTP
When creating a repository over SFTP, restic created the repository directories
with the SFTP server's default permissions, often 0755, rather than the 0700
permissions it uses for local repositories.
Restic now creates these directories with 0700 permissions.
https://github.com/restic/restic/issues/4447
https://github.com/restic/restic/pull/21817
* Bugfix #4467: Exit with code 3 when some `backup` source paths do not exist
Restic used to exit with code 0 when a top-level backup source path was missing,
and exited with code 3 only when a child path under an existing source did not
exist. Scripts that relied on the exit code could therefore treat an incomplete
backup as success.
Restic now exits with code 3 when any backup source path does not exist.
https://github.com/restic/restic/issues/4467
https://github.com/restic/restic/pull/5347
* Bugfix #4759: Error out when environment variables hold invalid values
If the environment variables `RESTIC_COMPRESSION`, `RESTIC_PACK_SIZE`, or
`RESTIC_READ_CONCURRENCY` could not be parsed, restic used to ignore them.
Restic now fails with an error unless the same setting is overridden on the
command line.
https://github.com/restic/restic/issues/4759
https://github.com/restic/restic/pull/5592
https://github.com/restic/restic/pull/5700
* Bugfix #5233: Return exit code 3 when failing to remove snapshots
Previously, the `forget` command returned exit code 0 when it failed to remove
one or more snapshots. This was misleading to scripts.
The `forget` command now instead returns exit code 3 when failing to remove one
or more snapshots.
https://github.com/restic/restic/issues/5233
https://github.com/restic/restic/pull/5322
* Bugfix #5258: Exit with code 130 on SIGINT
Restic used to return exit code 1 on SIGINT. It now returns 130, the usual
convention for a process stopped by Ctrl-C.
https://github.com/restic/restic/issues/5258
https://github.com/restic/restic/pull/5363
* Bugfix #5280: Reject impossible `find` time bounds immediately
The `find` command now fails with an error when both `--oldest` and `--newest`
are set and `--oldest` is later than `--newest`.
https://github.com/restic/restic/issues/5280
https://github.com/restic/restic/pull/5310
* Bugfix #5280: Make `find --pack` list blobs for tree packs
The `find --pack <tree-pack>` command now also reports blobs for packs that only
contain tree blobs.
https://github.com/restic/restic/issues/5280
https://github.com/restic/restic/pull/5664
* Bugfix #5354: Allow `rclone` and `sftp` backends when running in background
Previously, starting restic in the background could result in unexpected
behavior when using the `rclone` or `sftp` backends. For example, `restic -r
rclone:./example --insecure-no-password init &` could cause the calling `bash`
shell to exit unexpectedly.
This has now been fixed.
https://github.com/restic/restic/issues/5354
https://github.com/restic/restic/pull/5358
https://github.com/restic/restic/pull/5493
https://github.com/restic/restic/pull/5494
* Bugfix #5427: Correctly restore ACL inheritance state on Windows
Since security descriptor backups were added in restic 0.17.0, Access Control
Entry inheritance was not restored correctly on Windows; restored permissions
were always marked as explicit (not inherited) even when they were inherited
from a parent folder.
The inheritance flags are now correctly applied when restoring the security
descriptor, preserving the original permission structure.
https://github.com/restic/restic/issues/5427
https://github.com/restic/restic/pull/5465
* Bugfix #5477: Password prompt was sometimes not shown for `backup -v`
The repository password prompt could be hidden when running the `backup -v`
command. This has now been fixed.
https://github.com/restic/restic/issues/5477
https://github.com/restic/restic/pull/5554
* Bugfix #5487: Mark repository files read-only when using the SFTP backend
Files created through the SFTP backend previously stayed writable. New files now
get read-only permissions where the server supports `chmod`.
https://github.com/restic/restic/issues/5487
https://github.com/restic/restic/pull/5497
* Bugfix #5586: Correctly handle `snapshots --group-by` with `--latest`
For the `snapshots` command, `--latest` did not interact correctly with a
non-default `--group-by` value. This combination now behaves as intended.
https://github.com/restic/restic/issues/5586
https://github.com/restic/restic/pull/5601
* Bugfix #5595: Avoid spurious `chmod` errors on certain file backends
On filesystems that do not support `chmod` (for example CIFS or FUSE-mounted
WebDAV), restic since version 0.18.0 failed to remove stale locks, throwing the
error `chmod ...: operation not supported`. This has now been fixed.
https://github.com/restic/restic/issues/5595
https://github.com/restic/restic/pull/5596
* Bugfix #5683: Prevent `backup --stdin-from-command` from hanging
When using `--stdin-from-command`, the `backup` command could hang until
manually cancelled if the backup stopped before all subprocess output was
consumed, for example after a failed upload to the repository. This has now been
fixed.
https://github.com/restic/restic/issues/5683
https://github.com/restic/restic/pull/21829
* Bugfix #5757: Respect `--user` and `--host` in `key passwd`
The `key passwd` command previously ignored the `--user` and `--host` options
and always stored the new key with the current user and host name. These options
are now honored.
https://github.com/restic/restic/issues/5757
https://github.com/restic/restic/pull/21781
* Bugfix #21820: Correct handling of duplicate index entries
Before restic 0.10.0, a bug could, in very rare cases, split information about a
pack file across multiple index files.
Since restic 0.17.0, any operation that rewrites the index (like `prune` or
`repair packs`) could lose part of that information, resulting in errors in
later `check` or `prune` runs. This can be fixed by running `repair packs`, and
only repositories using repository format version 1 might be affected.
Split pack index entries are no longer lost during index rewrites. The `check`
command now reports these cases as errors that can be fixed using the `repair
packs` command. On older restic versions, running `repair index` twice also
fixes the problem.
https://github.com/restic/restic/issues/21820
https://github.com/restic/restic/pull/21828
* Bugfix #21820: Correctly handle pack files missing from the index
The `repair packs` command was unable to salvage blobs from a pack file if the
pack file was not contained in the index or the index entry was incomplete.
The command now uses information from both the index and the pack file header.
https://github.com/restic/restic/issues/21820
https://github.com/restic/restic/pull/21827
* Change #5293: Prune small packfiles more aggressively
The `prune` command now repacks more small packfiles by default. The option
`--repack-small` is no longer needed and has been marked as deprecated. The
`--repack-smaller-than` option can still be used to further control repacking of
small pack files.
https://github.com/restic/restic/issues/5293
https://github.com/restic/restic/pull/21803
* Change #5767: Prevent excluding paths explicitly passed to `backup`
When e.g. `restic backup --exclude-if-present .git /home/user/data` was run and
`/home/user/.git` existed, restic excluded the `data` directory from the
snapshot. The same applied to `--exclude-caches` or `--one-file-system`.
Similarly, `restic backup --exclude-larger-than 1M large-file.bin` produced an
empty snapshot when the file was larger than one megabyte.
The `backup` command now tracks which files and directories were specified on
the command line and does not apply excludes to those paths. Content inside a
backed-up directory is still filtered by excludes as before.
https://github.com/restic/restic/issues/5767
https://github.com/restic/restic/pull/21797
* Change #21791: Update dependencies and require Go 1.25 or newer
Dependencies have been updated. Building restic now requires Go 1.25 or newer.
The Windows build with Go 1.26 was also fixed.
https://github.com/restic/restic/issues/21791
https://github.com/restic/restic/pull/5619
https://github.com/restic/restic/pull/21796
* Enhancement #3326: Limit `check` to snapshots selected by filters
The `check` command can now restrict pack verification to snapshots chosen with
the usual snapshot filters (`--tag`, `--host`, `--path`, or explicit snapshot
IDs on the command line).
https://github.com/restic/restic/issues/3326
https://github.com/restic/restic/pull/5469
https://github.com/restic/restic/pull/5644
* Enhancement #3572: Support restoring ownership by name on UNIX systems
The `restore` command used to restore file ownership on UNIX systems by UID and
GID. It now supports restoring ownership by user and group name with
`--ownership-by-name`, so that snapshots can be restored on systems where
numeric IDs do not match those on the backup host.
Note: POSIX ACLs are still restored by numeric value; this change does not add
ACL-by-name support.
https://github.com/restic/restic/issues/3572
https://github.com/restic/restic/pull/5449
* Enhancement #3738: Optional GitHub token for `self-update` API requests
The `self-update` command used only unauthenticated GitHub API requests when
checking for releases. Shared IP addresses could hit the GitHub rate limit,
resulting in a 403 Forbidden error and preventing updates.
Unauthenticated requests remain the default, but authenticated requests are now
possible. Set the environment variable `GITHUB_ACCESS_TOKEN` to a GitHub
[personal access token](https://github.com/settings/tokens) to avoid rate-limit
failures.
https://github.com/restic/restic/issues/3738
https://github.com/restic/restic/pull/5568
* Enhancement #4278: Support include filters in the `rewrite` command
The `rewrite` command now accepts the same include filter options as the
`restore` command (`--include`, `--include-file`, `--iinclude`,
`--iinclude-file`, and short `-i`). Include and exclude filter options are
mutually exclusive.
https://github.com/restic/restic/issues/4278
https://github.com/restic/restic/pull/5191
* Enhancement #4728: Support zstd compression levels `fastest` and `better`
Restic now supports the zstd compression modes `fastest` and `better`. Set the
environment variable `RESTIC_COMPRESSION` to `fastest` or `better`, or pass the
same values with the `--compression` option.
https://github.com/restic/restic/issues/4728
https://github.com/restic/restic/pull/5321
* Enhancement #4868: Include repository ID in the filesystem name used by `mount`
The filesystem exposed by the `mount` command now includes the repository ID in
its name. The ID is printed when opening a repository or can be read with
`restic cat config`.
```
$ df ./test-mount/
Filesystem 1K-blocks Used Available Use% Mounted on
restic:d3b07384d1 0 0 0 - /mnt/my-restic-repo
```
https://github.com/restic/restic/issues/4868
https://github.com/restic/restic/pull/5243
* Enhancement #5175: Add status counters to `copy` in verbose text output
The `copy` command now prints additional counters in text mode when `--verbose`
is set: blobs to copy, their on-disk size, and the number of pack files read
from the source repository.
https://github.com/restic/restic/issues/5175
https://github.com/restic/restic/pull/5319
* Enhancement #5352: Support excluding cloud-backed files on macOS
Previously, restic treated cloud-backed files (such as files stored on iCloud)
like normal local files, forcing a full download of placeholders and other
"meant to be cloud only" content during backups.
The `backup` command now supports `--exclude-cloud-files` (previously only
available on Windows) to skip those files on supported macOS versions. From
Sonoma (macOS 14.0) onward the option can prevent unwanted downloads. Older
macOS versions will still download the files during a backup run.
https://github.com/restic/restic/issues/5352
https://github.com/restic/restic/pull/5370
* Enhancement #5383: Reduce progress bar refresh rates to decrease energy usage
Progress bars were previously updated at 60 frames per second, which could cause
high CPU or GPU usage in some terminal emulators.
The refresh rate is now 10 FPS to conserve energy. For some terminal emulators,
the lower rate is also necessary to allow selecting text in the terminal.
https://github.com/restic/restic/issues/5383
https://github.com/restic/restic/pull/5551
https://github.com/restic/restic/pull/5626
* Enhancement #5424: Enable Windows filesystem privileges before file access
Restic used to enable some Windows filesystem privileges only while reading or
writing security descriptors. Extended attributes could therefore be read before
enabling the backup privilege, possibly resulting in missed data or errors.
Restic now enables the relevant filesystem privileges before any file access.
https://github.com/restic/restic/pull/5424
* Enhancement #5440: Make `--host` override environment variable `RESTIC_HOST`
Previously, when the environment variable `RESTIC_HOST` was set, snapshot
listings and other operations were always filtered to that host.
Passing `--host` as an empty string (`--host=""` or `--host=`) now overrides
`RESTIC_HOST` and shows snapshots from all hosts.
The same override applies to other commands that support snapshot filters,
including `snapshots`, `forget`, `find`, `stats`, `copy`, `tag`, `repair
snapshots`, `rewrite`, `mount`, `restore`, `dump`, and `ls`.
https://github.com/restic/restic/issues/5440
https://github.com/restic/restic/pull/5541
* Enhancement #5448: Support configuring `nice` and `ionice` in the Docker image
The container entrypoint now reads optional scheduling hints from the
environment:
- The environment variable `NICE` sets the process nice value (see `man nice`).
- The environment variable `IONICE_CLASS` selects the I/O scheduling class (see
`man ionice`). Real-time classes need the `SYS_NICE` capability added to the
container.
- The environment variable `IONICE_PRIORITY` sets the priority within
`IONICE_CLASS` and has no effect unless `IONICE_CLASS` is set; it defaults to
`4` (neutral priority).
For further details, please see:
https://restic.readthedocs.io/en/stable/020_installation.html#docker-container
https://github.com/restic/restic/pull/5448
* Enhancement #5453: Copy multiple snapshots in batches
The `copy` command used to copy snapshots one at a time, even when doing so
produced pack files smaller than the target pack size. This led to many small
files when copying small incremental snapshots.
The `copy` command now copies multiple snapshots together so that small pack
files are avoided where possible.
https://github.com/restic/restic/issues/5453
https://github.com/restic/restic/pull/5472
* Enhancement #5523: Add Open Container Initiative labels to release Docker image
The release Docker image now includes OCI-style image annotation labels, which
helps external tooling identify the image.
https://github.com/restic/restic/pull/5523
* Enhancement #5531: Reduce Azure storage costs by optimizing uploads
Restic previously used Azure PutBlock and PutBlockList for every upload, which
cost two storage transactions per file and roughly doubled transaction charges
for repositories with many pack files.
Files up to 256 MiB now use PutBlob, requiring only a single transaction per
file and cutting typical transaction costs by about half. Larger blobs still use
block uploads as required by Azure.
https://github.com/restic/restic/issues/5531
https://github.com/restic/restic/pull/5544
* Enhancement #5562: Rewrite only changed status lines each frame
The status bar rewrote every line on each frame whenever any content changed,
which made selecting text impossible in some terminal emulators even when most
lines were unchanged.
Now only lines that actually change are rewritten on each update.
https://github.com/restic/restic/issues/5562
https://github.com/restic/restic/pull/5648
* Enhancement #5588: Show timezone context in `snapshots` output
The `snapshots` command now prints which timezone is used for displayed
timestamps. Snapshots may have been created in different timezones but are shown
in the local timezone, so a footer line (for example, timestamps shown in CET)
clarifies the display context when comparing snapshots from several sources.
https://github.com/restic/restic/pull/5588
* Enhancement #5610: Reduce `check`, `copy`, `diff` and `stats` memory usage
The `check`, `copy`, `diff` and `stats` commands now use less memory when
handling large snapshots.
https://github.com/restic/restic/pull/5610
* Enhancement #5689: Show more detailed progress for `stats`
The `stats` command used to show progress only while loading the index. During
the scan it printed only `scanning...` with no further updates. It now reports
how many snapshots, files, and blobs have been processed so far.
https://github.com/restic/restic/issues/5689
https://github.com/restic/restic/pull/5705
* Enhancement #5713: Significantly speed up index loading
Loading the index for a large repository is now significantly faster. Also, the
`mount` command now loads the index once at startup and then only loads new
index files as they appear. It also loads snapshots before printing that the
repository is being served.
https://github.com/restic/restic/pull/5713
https://github.com/restic/restic/pull/5720
* Enhancement #5718: Stricter and earlier validation of the `mount` point
The `mount` command previously accepted invalid mount points, resulting in an
error after loading the repository. The specified mount point must now refer to
a directory that the current user can access and write to, and this check is
performed before opening the repository.
https://github.com/restic/restic/pull/5718
# Changelog for restic 0.18.1 (2025-09-21) # Changelog for restic 0.18.1 (2025-09-21)
The following sections list the changes in restic 0.18.1 relevant to The following sections list the changes in restic 0.18.1 relevant to
restic users. The changes are ordered by importance. restic users. The changes are ordered by importance.
@@ -707,8 +1330,9 @@ restic users. The changes are ordered by importance.
correctly. correctly.
Specifying volume names is now handled correctly. To restore snapshots created Specifying volume names is now handled correctly. To restore snapshots created
before this bugfix, use the <snapshot>:<subpath> syntax. For example, to restore before this bugfix, use the <snapshot>:<subfolder> syntax. For example, to
a snapshot with ID `12345678` that backed up `C:`, use the following command: restore a snapshot with ID `12345678` that backed up `C:`, use the following
command:
``` ```
restic restore 12345678:/C/C:./ --target output/folder restic restore 12345678:/C/C:./ --target output/folder
+3
View File
@@ -202,6 +202,9 @@ we'll be glad to assist. Having a PR with failing integration tests is nothing
to be ashamed of. In contrast, that happens regularly for all of us. That's to be ashamed of. In contrast, that happens regularly for all of us. That's
what the tests are there for. what the tests are there for.
More details of how to structure tests can be found here at
[writing tests](https://restic.readthedocs.io/en/stable/090_participating.html#writing-tests).
Git Commits Git Commits
----------- -----------
+1 -1
View File
@@ -1 +1 @@
0.18.1 0.19.1-dev
+1 -2
View File
@@ -36,7 +36,6 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build ignore_build_go //go:build ignore_build_go
// +build ignore_build_go
package main package main
@@ -60,7 +59,7 @@ var config = Config{
// see https://github.com/googleapis/google-cloud-go/issues/11448 // see https://github.com/googleapis/google-cloud-go/issues/11448
DefaultBuildTags: []string{"selfupdate", "disable_grpc_modules"}, // specify build tags which are always used DefaultBuildTags: []string{"selfupdate", "disable_grpc_modules"}, // specify build tags which are always used
Tests: []string{"./..."}, // tests to run Tests: []string{"./..."}, // tests to run
MinVersion: GoVersion{Major: 1, Minor: 23, Patch: 0}, // minimum Go version supported MinVersion: GoVersion{Major: 1, Minor: 25, Patch: 0}, // minimum Go version supported
} }
// Config configures the build. // Config configures the build.
+1 -1
View File
@@ -6,7 +6,7 @@ resulting snapshot would result in an error. Note that using `C:\`
as backup target worked correctly. as backup target worked correctly.
Specifying volume names is now handled correctly. To restore snapshots Specifying volume names is now handled correctly. To restore snapshots
created before this bugfix, use the <snapshot>:<subpath> syntax. For created before this bugfix, use the <snapshot>:<subfolder> syntax. For
example, to restore a snapshot with ID `12345678` that backed up `C:`, example, to restore a snapshot with ID `12345678` that backed up `C:`,
use the following command: use the following command:
+17
View File
@@ -0,0 +1,17 @@
Bugfix: Correct handling of duplicate index entries
Before restic 0.10.0, a bug could, in very rare cases, split information
about a pack file across multiple index files.
Since restic 0.17.0, any operation that rewrites the index (like `prune`
or `repair packs`) could lose part of that information, resulting in errors
in later `check` or `prune` runs. This can be fixed by running `repair packs`,
and only repositories using repository format version 1 might be affected.
Split pack index entries are no longer lost during index rewrites. The
`check` command now reports these cases as errors that can be fixed using
the `repair packs` command. On older restic versions, running `repair index`
twice also fixes the problem.
https://github.com/restic/restic/issues/21820
https://github.com/restic/restic/pull/21828
+9
View File
@@ -0,0 +1,9 @@
Enhancement: Limit `check` to snapshots selected by filters
The `check` command can now restrict pack verification to snapshots chosen with
the usual snapshot filters (`--tag`, `--host`, `--path`, or explicit snapshot
IDs on the command line).
https://github.com/restic/restic/issues/3326
https://github.com/restic/restic/pull/5469
https://github.com/restic/restic/pull/5644
+12
View File
@@ -0,0 +1,12 @@
Enhancement: Support restoring ownership by name on UNIX systems
The `restore` command used to restore file ownership on UNIX systems by UID
and GID. It now supports restoring ownership by user and group name with
`--ownership-by-name`, so that snapshots can be restored on systems where
numeric IDs do not match those on the backup host.
Note: POSIX ACLs are still restored by numeric value; this change does not
add ACL-by-name support.
https://github.com/restic/restic/issues/3572
https://github.com/restic/restic/pull/5449
+13
View File
@@ -0,0 +1,13 @@
Enhancement: Optional GitHub token for `self-update` API requests
The `self-update` command used only unauthenticated GitHub API requests when
checking for releases. Shared IP addresses could hit the GitHub rate limit,
resulting in a 403 Forbidden error and preventing updates.
Unauthenticated requests remain the default, but authenticated requests are now
possible. Set the environment variable `GITHUB_ACCESS_TOKEN` to a GitHub
[personal access token](https://github.com/settings/tokens) to avoid rate-limit
failures.
https://github.com/restic/restic/issues/3738
https://github.com/restic/restic/pull/5568
+8
View File
@@ -0,0 +1,8 @@
Enhancement: Support include filters in the `rewrite` command
The `rewrite` command now accepts the same include filter options as the `restore`
command (`--include`, `--include-file`, `--iinclude`, `--iinclude-file`, and
short `-i`). Include and exclude filter options are mutually exclusive.
https://github.com/restic/restic/issues/4278
https://github.com/restic/restic/pull/5191
+10
View File
@@ -0,0 +1,10 @@
Bugfix: Use mode 0700 for repository directories created over SFTP
When creating a repository over SFTP, restic created the repository directories
with the SFTP server's default permissions, often 0755, rather than the 0700
permissions it uses for local repositories.
Restic now creates these directories with 0700 permissions.
https://github.com/restic/restic/issues/4447
https://github.com/restic/restic/pull/21817
+11
View File
@@ -0,0 +1,11 @@
Bugfix: Exit with code 3 when some `backup` source paths do not exist
Restic used to exit with code 0 when a top-level backup source path was
missing, and exited with code 3 only when a child path under an existing
source did not exist. Scripts that relied on the exit code could therefore
treat an incomplete backup as success.
Restic now exits with code 3 when any backup source path does not exist.
https://github.com/restic/restic/issues/4467
https://github.com/restic/restic/pull/5347
+8
View File
@@ -0,0 +1,8 @@
Enhancement: Support zstd compression levels `fastest` and `better`
Restic now supports the zstd compression modes `fastest` and `better`. Set the
environment variable `RESTIC_COMPRESSION` to `fastest` or `better`, or pass the
same values with the `--compression` option.
https://github.com/restic/restic/issues/4728
https://github.com/restic/restic/pull/5321
+10
View File
@@ -0,0 +1,10 @@
Bugfix: Error out when environment variables hold invalid values
If the environment variables `RESTIC_COMPRESSION`, `RESTIC_PACK_SIZE`, or
`RESTIC_READ_CONCURRENCY` could not be parsed, restic used to ignore them.
Restic now fails with an error unless the same setting is overridden on the
command line.
https://github.com/restic/restic/issues/4759
https://github.com/restic/restic/pull/5592
https://github.com/restic/restic/pull/5700
+14
View File
@@ -0,0 +1,14 @@
Enhancement: Include repository ID in the filesystem name used by `mount`
The filesystem exposed by the `mount` command now includes the repository ID
in its name. The ID is printed when opening a repository or can be read with
`restic cat config`.
```
$ df ./test-mount/
Filesystem 1K-blocks Used Available Use% Mounted on
restic:d3b07384d1 0 0 0 - /mnt/my-restic-repo
```
https://github.com/restic/restic/issues/4868
https://github.com/restic/restic/pull/5243
+10
View File
@@ -0,0 +1,10 @@
Bugfix: Return exit code 3 when failing to remove snapshots
Previously, the `forget` command returned exit code 0 when it failed
to remove one or more snapshots. This was misleading to scripts.
The `forget` command now instead returns exit code 3 when failing to
remove one or more snapshots.
https://github.com/restic/restic/issues/5233
https://github.com/restic/restic/pull/5322
+7
View File
@@ -0,0 +1,7 @@
Bugfix: Exit with code 130 on SIGINT
Restic used to return exit code 1 on SIGINT. It now returns 130, the usual
convention for a process stopped by Ctrl-C.
https://github.com/restic/restic/issues/5258
https://github.com/restic/restic/pull/5363
+7
View File
@@ -0,0 +1,7 @@
Bugfix: Reject impossible `find` time bounds immediately
The `find` command now fails with an error when both `--oldest`
and `--newest` are set and `--oldest` is later than `--newest`.
https://github.com/restic/restic/issues/5280
https://github.com/restic/restic/pull/5310
+9
View File
@@ -0,0 +1,9 @@
Change: Prune small packfiles more aggressively
The `prune` command now repacks more small packfiles by default. The option
`--repack-small` is no longer needed and has been marked as deprecated. The
`--repack-smaller-than` option can still be used to further control repacking
of small pack files.
https://github.com/restic/restic/issues/5293
https://github.com/restic/restic/pull/21803
+13
View File
@@ -0,0 +1,13 @@
Enhancement: Support excluding cloud-backed files on macOS
Previously, restic treated cloud-backed files (such as files stored on iCloud)
like normal local files, forcing a full download of placeholders and other
"meant to be cloud only" content during backups.
The `backup` command now supports `--exclude-cloud-files` (previously only
available on Windows) to skip those files on supported macOS versions. From
Sonoma (macOS 14.0) onward the option can prevent unwanted downloads. Older
macOS versions will still download the files during a backup run.
https://github.com/restic/restic/issues/5352
https://github.com/restic/restic/pull/5370
+13
View File
@@ -0,0 +1,13 @@
Bugfix: Allow `rclone` and `sftp` backends when running in background
Previously, starting restic in the background could result in unexpected
behavior when using the `rclone` or `sftp` backends. For example,
`restic -r rclone:./example --insecure-no-password init &` could cause the
calling `bash` shell to exit unexpectedly.
This has now been fixed.
https://github.com/restic/restic/issues/5354
https://github.com/restic/restic/pull/5358
https://github.com/restic/restic/pull/5493
https://github.com/restic/restic/pull/5494
+12
View File
@@ -0,0 +1,12 @@
Enhancement: Reduce progress bar refresh rates to decrease energy usage
Progress bars were previously updated at 60 frames per second, which
could cause high CPU or GPU usage in some terminal emulators.
The refresh rate is now 10 FPS to conserve energy. For some terminal
emulators, the lower rate is also necessary to allow selecting text
in the terminal.
https://github.com/restic/restic/issues/5383
https://github.com/restic/restic/pull/5551
https://github.com/restic/restic/pull/5626
+14
View File
@@ -0,0 +1,14 @@
Enhancement: Make `--host` override environment variable `RESTIC_HOST`
Previously, when the environment variable `RESTIC_HOST` was set, snapshot
listings and other operations were always filtered to that host.
Passing `--host` as an empty string (`--host=""` or `--host=`) now overrides
`RESTIC_HOST` and shows snapshots from all hosts.
The same override applies to other commands that support snapshot filters,
including `snapshots`, `forget`, `find`, `stats`, `copy`, `tag`,
`repair snapshots`, `rewrite`, `mount`, `restore`, `dump`, and `ls`.
https://github.com/restic/restic/issues/5440
https://github.com/restic/restic/pull/5541
+11
View File
@@ -0,0 +1,11 @@
Enhancement: Copy multiple snapshots in batches
The `copy` command used to copy snapshots one at a time, even when doing so
produced pack files smaller than the target pack size. This led to many small
files when copying small incremental snapshots.
The `copy` command now copies multiple snapshots together so that small pack
files are avoided where possible.
https://github.com/restic/restic/issues/5453
https://github.com/restic/restic/pull/5472
+7
View File
@@ -0,0 +1,7 @@
Bugfix: Password prompt was sometimes not shown for `backup -v`
The repository password prompt could be hidden when running the
`backup -v` command. This has now been fixed.
https://github.com/restic/restic/issues/5477
https://github.com/restic/restic/pull/5554
+7
View File
@@ -0,0 +1,7 @@
Bugfix: Mark repository files read-only when using the SFTP backend
Files created through the SFTP backend previously stayed writable. New files now
get read-only permissions where the server supports `chmod`.
https://github.com/restic/restic/issues/5487
https://github.com/restic/restic/pull/5497
+12
View File
@@ -0,0 +1,12 @@
Enhancement: Reduce Azure storage costs by optimizing uploads
Restic previously used Azure PutBlock and PutBlockList for every upload, which
cost two storage transactions per file and roughly doubled transaction charges
for repositories with many pack files.
Files up to 256 MiB now use PutBlob, requiring only a single transaction per
file and cutting typical transaction costs by about half. Larger blobs still
use block uploads as required by Azure.
https://github.com/restic/restic/issues/5531
https://github.com/restic/restic/pull/5544
+10
View File
@@ -0,0 +1,10 @@
Enhancement: Rewrite only changed status lines each frame
The status bar rewrote every line on each frame whenever any content changed,
which made selecting text impossible in some terminal emulators even when most
lines were unchanged.
Now only lines that actually change are rewritten on each update.
https://github.com/restic/restic/issues/5562
https://github.com/restic/restic/pull/5648
+7
View File
@@ -0,0 +1,7 @@
Bugfix: Correctly handle `snapshots --group-by` with `--latest`
For the `snapshots` command, `--latest` did not interact correctly with a
non-default `--group-by` value. This combination now behaves as intended.
https://github.com/restic/restic/issues/5586
https://github.com/restic/restic/pull/5601
+8
View File
@@ -0,0 +1,8 @@
Bugfix: Avoid spurious `chmod` errors on certain file backends
On filesystems that do not support `chmod` (for example CIFS or FUSE-mounted
WebDAV), restic since version 0.18.0 failed to remove stale locks, throwing
the error `chmod ...: operation not supported`. This has now been fixed.
https://github.com/restic/restic/issues/5595
https://github.com/restic/restic/pull/5596
+9
View File
@@ -0,0 +1,9 @@
Bugfix: Prevent `backup --stdin-from-command` from hanging
When using `--stdin-from-command`, the `backup` command could hang until
manually cancelled if the backup stopped before all subprocess output was
consumed, for example after a failed upload to the repository. This has
now been fixed.
https://github.com/restic/restic/issues/5683
https://github.com/restic/restic/pull/21829
+8
View File
@@ -0,0 +1,8 @@
Enhancement: Show more detailed progress for `stats`
The `stats` command used to show progress only while loading the index. During
the scan it printed only `scanning...` with no further updates. It now reports
how many snapshots, files, and blobs have been processed so far.
https://github.com/restic/restic/issues/5689
https://github.com/restic/restic/pull/5705
+8
View File
@@ -0,0 +1,8 @@
Bugfix: Respect `--user` and `--host` in `key passwd`
The `key passwd` command previously ignored the `--user` and
`--host` options and always stored the new key with the current
user and host name. These options are now honored.
https://github.com/restic/restic/issues/5757
https://github.com/restic/restic/pull/21781
+15
View File
@@ -0,0 +1,15 @@
Change: Prevent excluding paths explicitly passed to `backup`
When e.g. `restic backup --exclude-if-present .git /home/user/data` was run
and `/home/user/.git` existed, restic excluded the `data` directory from the
snapshot. The same applied to `--exclude-caches` or `--one-file-system`.
Similarly, `restic backup --exclude-larger-than 1M large-file.bin` produced
an empty snapshot when the file was larger than one megabyte.
The `backup` command now tracks which files and directories were specified on
the command line and does not apply excludes to those paths. Content inside a
backed-up directory is still filtered by excludes as before.
https://github.com/restic/restic/issues/5767
https://github.com/restic/restic/pull/21797
+9
View File
@@ -0,0 +1,9 @@
Bugfix: Support serving a `restic mount` of a Windows system via Samba
A repository mounted using `restic mount` on a POSIX system could not use
Samba to serve files from backups of Windows systems, while backups of
non-Windows systems could be served successfully. This has now been fixed.
https://github.com/restic/restic/issues/2034
https://github.com/restic/restic/issues/4382
https://github.com/restic/restic/pull/21784
+8
View File
@@ -0,0 +1,8 @@
Change: Update dependencies and require Go 1.25 or newer
Dependencies have been updated. Building restic now requires
Go 1.25 or newer. The Windows build with Go 1.26 was also fixed.
https://github.com/restic/restic/issues/21791
https://github.com/restic/restic/pull/5619
https://github.com/restic/restic/pull/21796
+9
View File
@@ -0,0 +1,9 @@
Bugfix: Correctly handle pack files missing from the index
The `repair packs` command was unable to salvage blobs from a pack file if
the pack file was not contained in the index or the index entry was incomplete.
The command now uses information from both the index and the pack file header.
https://github.com/restic/restic/issues/21820
https://github.com/restic/restic/pull/21827
+8
View File
@@ -0,0 +1,8 @@
Enhancement: Add status counters to `copy` in verbose text output
The `copy` command now prints additional counters in text mode when
`--verbose` is set: blobs to copy, their on-disk size, and the number
of pack files read from the source repository.
https://github.com/restic/restic/issues/5175
https://github.com/restic/restic/pull/5319
+10
View File
@@ -0,0 +1,10 @@
Enhancement: Enable Windows filesystem privileges before file access
Restic used to enable some Windows filesystem privileges only while reading
or writing security descriptors. Extended attributes could therefore be read
before enabling the backup privilege, possibly resulting in missed data or
errors.
Restic now enables the relevant filesystem privileges before any file access.
https://github.com/restic/restic/pull/5424
+19
View File
@@ -0,0 +1,19 @@
Enhancement: Support configuring `nice` and `ionice` in the Docker image
The container entrypoint now reads optional scheduling hints from the
environment:
- The environment variable `NICE` sets the process nice value (see `man nice`).
- The environment variable `IONICE_CLASS` selects the I/O scheduling class (see
`man ionice`). Real-time classes need the `SYS_NICE` capability added to the
container.
- The environment variable `IONICE_PRIORITY` sets the priority within
`IONICE_CLASS` and has no effect unless `IONICE_CLASS` is set; it defaults to
`4` (neutral priority).
For further details, please see:
https://restic.readthedocs.io/en/stable/020_installation.html#docker-container
https://github.com/restic/restic/pull/5448
+12
View File
@@ -0,0 +1,12 @@
Bugfix: Correctly restore ACL inheritance state on Windows
Since security descriptor backups were added in restic 0.17.0, Access Control
Entry inheritance was not restored correctly on Windows; restored permissions
were always marked as explicit (not inherited) even when they were inherited
from a parent folder.
The inheritance flags are now correctly applied when restoring the security
descriptor, preserving the original permission structure.
https://github.com/restic/restic/issues/5427
https://github.com/restic/restic/pull/5465
+6
View File
@@ -0,0 +1,6 @@
Enhancement: Add Open Container Initiative labels to release Docker image
The release Docker image now includes OCI-style image annotation labels,
which helps external tooling identify the image.
https://github.com/restic/restic/pull/5523
+9
View File
@@ -0,0 +1,9 @@
Enhancement: Show timezone context in `snapshots` output
The `snapshots` command now prints which timezone is used for displayed
timestamps. Snapshots may have been created in different timezones but are
shown in the local timezone, so a footer line (for example, timestamps shown in
CET) clarifies the display context when comparing snapshots from several
sources.
https://github.com/restic/restic/pull/5588
+6
View File
@@ -0,0 +1,6 @@
Enhancement: Reduce `check`, `copy`, `diff` and `stats` memory usage
The `check`, `copy`, `diff` and `stats` commands now use less memory
when handling large snapshots.
https://github.com/restic/restic/pull/5610
+7
View File
@@ -0,0 +1,7 @@
Bugfix: Make `find --pack` list blobs for tree packs
The `find --pack <tree-pack>` command now also reports
blobs for packs that only contain tree blobs.
https://github.com/restic/restic/issues/5280
https://github.com/restic/restic/pull/5664
+8
View File
@@ -0,0 +1,8 @@
Enhancement: Stricter and earlier validation of the `mount` point
The `mount` command previously accepted invalid mount points, resulting in an
error after loading the repository. The specified mount point must now refer to
a directory that the current user can access and write to, and this check is
performed before opening the repository.
https://github.com/restic/restic/pull/5718
+9
View File
@@ -0,0 +1,9 @@
Enhancement: Significantly speed up index loading
Loading the index for a large repository is now significantly faster. Also,
the `mount` command now loads the index once at startup and then only loads
new index files as they appear. It also loads snapshots before printing that
the repository is being served.
https://github.com/restic/restic/pull/5713
https://github.com/restic/restic/pull/5720
+8
View File
@@ -0,0 +1,8 @@
Bugfix: Hide `stats` progress bar in JSON mode
Since restic 0.19.0, the `stats` command shows a progress bar. This
progress bar was unintentionally displayed also when using the `--json`
option, mixing regular text output with JSON. This is now fixed.
https://github.com/restic/restic/issues/21866
https://github.com/restic/restic/pull/21871
+12
View File
@@ -0,0 +1,12 @@
Bugfix: Restore old behavior of `snapshots --latest <n>` without `--group-by`
Restic 0.19.0 accidentally changed the behavior of `snapshots --latest <n>`
to no longer group snapshots by host and paths by default.
The `snapshots --latest <n>` command now again uses the old behavior of
grouping by host and paths when `--group-by` is not specified. However, when
specifying `--group-by` the output is still grouped as requested, as in restic
0.19.0.
https://github.com/restic/restic/issues/21869
https://github.com/restic/restic/pull/21875
+9
View File
@@ -0,0 +1,9 @@
Bugfix: Remove read-only files via the SFTP backend on Windows servers
Since restic 0.19.0, repository files on the SFTP backend are marked
read-only after save. On Windows SFTP servers, removing them failed
with a permission error. The SFTP backend now clears the read-only flag
before removing the file.
https://github.com/restic/restic/issues/21895
https://github.com/restic/restic/pull/21897
+8
View File
@@ -0,0 +1,8 @@
Bugfix: Make `backup` respect excludes for duplicate directory entries
Since restic 0.19.0, backing up a directory with duplicate directory
entries always produced "Warning: at least one source file could not be
read", even when those files were excluded. This has now been fixed.
https://github.com/restic/restic/issues/21899
https://github.com/restic/restic/pull/21900
+12
View File
@@ -0,0 +1,12 @@
Bugfix: Prevent mounting over the repository directory
Using a local repository directory as the `mount` target — or a path
that contains it, or that it contains — caused the FUSE server to
read its own backend files through the new mount, deadlocking the
kernel and requiring a long reboot to recover.
Restic now resolves both paths and refuses any such overlap with a
clear error before mounting.
https://github.com/restic/restic/issues/5234
https://github.com/restic/restic/pull/5348
+9
View File
@@ -0,0 +1,9 @@
Bugfix: Skip inaccessible `backup` source paths
The `backup` command only skipped source paths that did not exist. A path that
could not be accessed for another reason, such as a malformed path on Windows,
was kept and produced an empty snapshot. Restic now skips any such path and
aborts if none remain.
https://github.com/restic/restic/issues/5667
https://github.com/restic/restic/pull/21852
+10
View File
@@ -0,0 +1,10 @@
Bugfix: Update `mount` latest symlink after snapshot reload
When `restic mount` was kept running while new snapshots were
created, the new snapshots appeared in the mountpoint, but the `latest`
symlink could still point to the previously latest snapshot. Restic now
invalidates the cached snapshot directory entries after a snapshot reload so
that `latest` points to the newest snapshot.
https://github.com/restic/restic/issues/5722
https://github.com/restic/restic/pull/21873
+9
View File
@@ -0,0 +1,9 @@
Bugfix: Show timezone location in `snapshots` output
With restic 0.19.0, the `snapshots` command printed the current timezone when
listing snapshots. However, that timezone label might change during the year,
for example with daylight saving time. Restic now prints a more consistent and
shorter version of the text.
https://github.com/restic/restic/pull/21876
https://forum.restic.net/t/possible-bug-in-timezone-naming/10867
+7
View File
@@ -0,0 +1,7 @@
Bugfix: Prevent crash in mountpoint validation if mountpoint is inaccessible
Since restic 0.19.0, the `mount` command validates a mountpoint before loading
the repository. If restic was unable to stat the mountpoint, this would result
in a crash. This has now been fixed to correctly return an error instead.
https://github.com/restic/restic/pull/21879
+11 -5
View File
@@ -1,14 +1,20 @@
# The first line must start with Bugfix:, Enhancement: or Change:, # The first line must start with Bugfix:, Enhancement: or Change:,
# including the colon. Use present tense and the imperative mood. Remove # including the colon. 'Change:' is for breaking changes only.
# lines starting with '#' from this template. # Documentation-only changes do not get a changelog entry.
Enhancement: Allow custom bar in the foo command # Include the affected command in the summary if relevant.
#
# Use present tense and the imperative mood. Remove lines starting
# with '#' from this template.
Enhancement: Allow custom bar in the `foo` command
# Describe the problem in the past tense, the new behavior in the present # Describe the problem in the past tense, the new behavior in the present
# tense. Mention the affected commands, backends, operating systems, etc. # tense. Mention the affected commands, backends, operating systems, etc.
# If the problem description just says that a feature was missing, then # If the problem description just says that a feature was missing, then
# only explain the new behavior. # only explain the new behavior. Aim for a short and concise description.
# Focus on user-facing behavior, not the implementation.
# Use "Restic now ..." instead of "We have changed ...". # Use "Restic now ..." instead of "We have changed ...".
#
# Focus on user-facing behavior, not the implementation. The description should
# be understandable for a regular user without knowledge of the implementation.
Restic foo always used the system-wide bar when deciding how to frob an Restic foo always used the system-wide bar when deciding how to frob an
item in the `baz` backend. It now permits selecting the bar with `--bar` item in the `baz` backend. It now permits selecting the bar with `--bar`
+10 -7
View File
@@ -2,6 +2,8 @@ package main
import ( import (
"context" "context"
"fmt"
"io"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@@ -9,26 +11,27 @@ import (
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
) )
func createGlobalContext() context.Context { func createGlobalContext(stderr io.Writer) context.Context {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
ch := make(chan os.Signal, 1) ch := make(chan os.Signal, 1)
go cleanupHandler(ch, cancel) go cleanupHandler(ch, cancel, stderr)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
return ctx return ctx
} }
// cleanupHandler handles the SIGINT and SIGTERM signals. // cleanupHandler handles the SIGINT and SIGTERM signals.
func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc) { func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc, stderr io.Writer) {
s := <-c s := <-c
debug.Log("signal %v received, cleaning up", s) debug.Log("signal %v received, cleaning up", s)
Warnf("%ssignal %v received, cleaning up\n", clearLine(0), s) // ignore error as there's no good way to handle it
_, _ = fmt.Fprintf(stderr, "\rsignal %v received, cleaning up \n", s)
if val, _ := os.LookupEnv("RESTIC_DEBUG_STACKTRACE_SIGINT"); val != "" { if val, _ := os.LookupEnv("RESTIC_DEBUG_STACKTRACE_SIGINT"); val != "" {
_, _ = os.Stderr.WriteString("\n--- STACKTRACE START ---\n\n") _, _ = stderr.Write([]byte("\n--- STACKTRACE START ---\n\n"))
_, _ = os.Stderr.WriteString(debug.DumpStacktrace()) _, _ = stderr.Write([]byte(debug.DumpStacktrace()))
_, _ = os.Stderr.WriteString("\n--- STACKTRACE END ---\n") _, _ = stderr.Write([]byte("\n--- STACKTRACE END ---\n"))
} }
cancel() cancel()
+98 -84
View File
@@ -19,19 +19,20 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/filter"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/textfile" "github.com/restic/restic/internal/textfile"
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/backup" "github.com/restic/restic/internal/ui/backup"
"github.com/restic/restic/internal/ui/termstatus"
) )
func newBackupCommand() *cobra.Command { func newBackupCommand(globalOptions *global.Options) *cobra.Command {
var opts BackupOptions var opts BackupOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -51,22 +52,28 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. Exit status is 11 if the repository is already locked.
Exit status is 12 if the password is incorrect. Exit status is 12 if the password is incorrect.
`, `,
PreRun: func(_ *cobra.Command, _ []string) { 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 == "" { if opts.Host == "" {
hostname, err := os.Hostname() hostname, err := os.Hostname()
if err != nil { if err != nil {
debug.Log("os.Hostname() returned err: %v", err) debug.Log("os.Hostname() returned err: %v", err)
return return nil
} }
opts.Host = hostname opts.Host = hostname
} }
return nil
}, },
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() return runBackup(cmd.Context(), opts, *globalOptions, globalOptions.Term, args)
defer cancel()
return runBackup(cmd.Context(), opts, globalOptions, term, args)
}, },
} }
@@ -79,7 +86,7 @@ type BackupOptions struct {
filter.ExcludePatternOptions filter.ExcludePatternOptions
Parent string Parent string
GroupBy restic.SnapshotGroupByOptions GroupBy data.SnapshotGroupByOptions
Force bool Force bool
ExcludeOtherFS bool ExcludeOtherFS bool
ExcludeIfPresent []string ExcludeIfPresent []string
@@ -89,7 +96,7 @@ type BackupOptions struct {
Stdin bool Stdin bool
StdinFilename string StdinFilename string
StdinCommand bool StdinCommand bool
Tags restic.TagLists Tags data.TagLists
Host string Host string
FilesFrom []string FilesFrom []string
FilesFromVerbatim []string FilesFromVerbatim []string
@@ -103,11 +110,13 @@ type BackupOptions struct {
ReadConcurrency uint ReadConcurrency uint
NoScan bool NoScan bool
SkipIfUnchanged bool SkipIfUnchanged bool
readConcurrencyFlag *pflag.Flag
} }
func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) { func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
f.StringVar(&opts.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)") f.StringVar(&opts.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)")
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true} opts.GroupBy = data.SnapshotGroupByOptions{Host: true, Path: true}
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')") f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
f.BoolVarP(&opts.Force, "force", "f", false, `force re-reading the source files/directories (overrides the "parent" flag)`) f.BoolVarP(&opts.Force, "force", "f", false, `force re-reading the source files/directories (overrides the "parent" flag)`)
@@ -140,13 +149,13 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup") f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
f.BoolVar(&opts.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)") f.BoolVar(&opts.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
f.BoolVar(&opts.ExcludeCloudFiles, "exclude-cloud-files", false, "excludes online-only cloud files (such as OneDrive Files On-Demand)") }
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
f.BoolVar(&opts.ExcludeCloudFiles, "exclude-cloud-files", false, "excludes online-only cloud files (such as OneDrive, iCloud drive, …)")
} }
f.BoolVar(&opts.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot") f.BoolVar(&opts.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot")
// parse read concurrency from env, on error the default value will be used opts.readConcurrencyFlag = f.Lookup("read-concurrency")
readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32)
opts.ReadConcurrency = uint(readConcurrency)
// parse host from env, if not exists or empty the default value will be used // parse host from env, if not exists or empty the default value will be used
if host := os.Getenv("RESTIC_HOST"); host != "" { if host := os.Getenv("RESTIC_HOST"); host != "" {
@@ -159,13 +168,20 @@ var backupFSTestHook func(fs fs.FS) fs.FS
// ErrInvalidSourceData is used to report an incomplete backup // ErrInvalidSourceData is used to report an incomplete backup
var ErrInvalidSourceData = errors.New("at least one source file could not be read") var ErrInvalidSourceData = errors.New("at least one source file could not be read")
// filterExisting returns a slice of all existing items, or an error if no // ErrNoSourceData is used to report that no source data was found
// items exist at all. var ErrNoSourceData = errors.Fatal("all source directories/files do not exist")
func filterExisting(items []string) (result []string, err error) {
// 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) {
for _, item := range items { for _, item := range items {
_, err := fs.Lstat(item) _, err := fs.Lstat(item)
if errors.Is(err, os.ErrNotExist) { if err != nil {
Warnf("%v does not exist, skipping\n", item) if errors.Is(err, os.ErrNotExist) {
warnf("%v does not exist, skipping\n", item)
} else {
warnf("%v cannot be accessed, skipping\n", item)
}
continue continue
} }
@@ -173,10 +189,12 @@ func filterExisting(items []string) (result []string, err error) {
} }
if len(result) == 0 { if len(result) == 0 {
return nil, errors.Fatal("all source directories/files do not exist") return nil, ErrNoSourceData
} else if len(result) < len(items) {
return result, ErrInvalidSourceData
} }
return return result, nil
} }
// readLines reads all lines from the named file and returns them as a // readLines reads all lines from the named file and returns them as a
@@ -185,7 +203,7 @@ func filterExisting(items []string) (result []string, err error) {
// If filename is empty, readPatternsFromFile returns an empty slice. // If filename is empty, readPatternsFromFile returns an empty slice.
// If filename is a dash (-), readPatternsFromFile will read the lines from the // If filename is a dash (-), readPatternsFromFile will read the lines from the
// standard input. // standard input.
func readLines(filename string) ([]string, error) { func readLines(filename string, stdin io.ReadCloser) ([]string, error) {
if filename == "" { if filename == "" {
return nil, nil return nil, nil
} }
@@ -196,7 +214,7 @@ func readLines(filename string) ([]string, error) {
) )
if filename == "-" { if filename == "-" {
data, err = io.ReadAll(os.Stdin) data, err = io.ReadAll(stdin)
} else { } else {
data, err = textfile.Read(filename) data, err = textfile.Read(filename)
} }
@@ -221,8 +239,8 @@ func readLines(filename string) ([]string, error) {
// readFilenamesFromFileRaw reads a list of filenames from the given file, // readFilenamesFromFileRaw reads a list of filenames from the given file,
// or stdin if filename is "-". Each filename is terminated by a zero byte, // or stdin if filename is "-". Each filename is terminated by a zero byte,
// which is stripped off. // which is stripped off.
func readFilenamesFromFileRaw(filename string) (names []string, err error) { func readFilenamesFromFileRaw(filename string, stdin io.ReadCloser) (names []string, err error) {
f := os.Stdin f := stdin
if filename != "-" { if filename != "-" {
if f, err = os.Open(filename); err != nil { if f, err = os.Open(filename); err != nil {
return nil, err return nil, err
@@ -271,8 +289,8 @@ func readFilenamesRaw(r io.Reader) (names []string, err error) {
} }
// Check returns an error when an invalid combination of options was set. // Check returns an error when an invalid combination of options was set.
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error { func (opts BackupOptions) Check(gopts global.Options, args []string) error {
if gopts.password == "" && !gopts.InsecureNoPassword { if gopts.Password == "" && !gopts.InsecureNoPassword {
if opts.Stdin { if opts.Stdin {
return errors.Fatal("cannot read both password and data from stdin") return errors.Fatal("cannot read both password and data from stdin")
} }
@@ -306,7 +324,7 @@ func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
// collectRejectByNameFuncs returns a list of all functions which may reject data // collectRejectByNameFuncs returns a list of all functions which may reject data
// from being saved in a snapshot based on path only // from being saved in a snapshot based on path only
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (fs []archiver.RejectByNameFunc, err error) { func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...interface{})) (fs []archiver.RejectByNameFunc, err error) {
// exclude restic cache // exclude restic cache
if repo.Cache() != nil { if repo.Cache() != nil {
f, err := rejectResticCache(repo) f, err := rejectResticCache(repo)
@@ -317,7 +335,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
fs = append(fs, f) fs = append(fs, f)
} }
fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(Warnf) fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(warnf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -330,7 +348,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
// collectRejectFuncs returns a list of all functions which may reject data // collectRejectFuncs returns a list of all functions which may reject data
// from being saved in a snapshot based on path and file info // from being saved in a snapshot based on path and file info
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs []archiver.RejectFunc, err error) { func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...interface{})) (funcs []archiver.RejectFunc, err error) {
// allowed devices // allowed devices
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand { if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
f, err := archiver.RejectByDevice(targets, fs) f, err := archiver.RejectByDevice(targets, fs)
@@ -354,10 +372,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
} }
if opts.ExcludeCloudFiles && !opts.Stdin && !opts.StdinCommand { if opts.ExcludeCloudFiles && !opts.Stdin && !opts.StdinCommand {
if runtime.GOOS != "windows" { f, err := archiver.RejectCloudFiles(warnf)
return nil, errors.Fatalf("exclude-cloud-files is only supported on Windows")
}
f, err := archiver.RejectCloudFiles(Warnf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -369,7 +384,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
} }
for _, spec := range opts.ExcludeIfPresent { for _, spec := range opts.ExcludeIfPresent {
f, err := archiver.RejectIfPresent(spec, Warnf) f, err := archiver.RejectIfPresent(spec, warnf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -381,13 +396,13 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
} }
// collectTargets returns a list of target files/dirs from several sources. // collectTargets returns a list of target files/dirs from several sources.
func collectTargets(opts BackupOptions, args []string) (targets []string, err error) { func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{}), stdin io.ReadCloser) (targets []string, err error) {
if opts.Stdin || opts.StdinCommand { if opts.Stdin || opts.StdinCommand {
return nil, nil return nil, nil
} }
for _, file := range opts.FilesFrom { for _, file := range opts.FilesFrom {
fromfile, err := readLines(file) fromfile, err := readLines(file, stdin)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -405,14 +420,14 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
return nil, fmt.Errorf("pattern: %s: %w", line, err) return nil, fmt.Errorf("pattern: %s: %w", line, err)
} }
if len(expanded) == 0 { if len(expanded) == 0 {
Warnf("pattern %q does not match any files, skipping\n", line) warnf("pattern %q does not match any files, skipping\n", line)
} }
targets = append(targets, expanded...) targets = append(targets, expanded...)
} }
} }
for _, file := range opts.FilesFromVerbatim { for _, file := range opts.FilesFromVerbatim {
fromfile, err := readLines(file) fromfile, err := readLines(file, stdin)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -425,7 +440,7 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
} }
for _, file := range opts.FilesFromRaw { for _, file := range opts.FilesFromRaw {
fromfile, err := readFilenamesFromFileRaw(file) fromfile, err := readFilenamesFromFileRaw(file, stdin)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -439,17 +454,12 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
return nil, errors.Fatal("nothing to backup, please specify source files/dirs") return nil, errors.Fatal("nothing to backup, please specify source files/dirs")
} }
targets, err = filterExisting(targets) return filterExisting(targets, warnf)
if err != nil {
return nil, err
}
return targets, nil
} }
// parent returns the ID of the parent snapshot. If there is none, nil is // parent returns the ID of the parent snapshot. If there is none, nil is
// returned. // returned.
func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, opts BackupOptions, targets []string, timeStampLimit time.Time) (*restic.Snapshot, error) { func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, opts BackupOptions, targets []string, timeStampLimit time.Time) (*data.Snapshot, error) {
if opts.Force { if opts.Force {
return nil, nil return nil, nil
} }
@@ -458,7 +468,7 @@ func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, o
if snName == "" { if snName == "" {
snName = "latest" snName = "latest"
} }
f := restic.SnapshotFilter{TimestampLimit: timeStampLimit} f := data.SnapshotFilter{TimestampLimit: timeStampLimit}
if opts.GroupBy.Host { if opts.GroupBy.Host {
f.Hosts = []string{opts.Host} f.Hosts = []string{opts.Host}
} }
@@ -466,23 +476,29 @@ func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, o
f.Paths = targets f.Paths = targets
} }
if opts.GroupBy.Tag { if opts.GroupBy.Tag {
f.Tags = []restic.TagList{opts.Tags.Flatten()} f.Tags = []data.TagList{opts.Tags.Flatten()}
} }
sn, _, err := f.FindLatest(ctx, repo, repo, snName) sn, _, err := f.FindLatest(ctx, repo, repo, snName)
// Snapshot not found is ok if no explicit parent was set // Snapshot not found is ok if no explicit parent was set
if opts.Parent == "" && errors.Is(err, restic.ErrNoSnapshotFound) { if opts.Parent == "" && errors.Is(err, data.ErrNoSnapshotFound) {
err = nil err = nil
} }
return sn, err return sn, err
} }
func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error { func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, term ui.Terminal, args []string) error {
var vsscfg fs.VSSConfig var vsscfg fs.VSSConfig
var err error var err error
var printer backup.ProgressPrinter
if gopts.JSON {
printer = backup.NewJSONProgress(term, gopts.Verbosity)
} else {
printer = backup.NewTextProgress(term, gopts.Verbosity)
}
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if vsscfg, err = fs.ParseVSSConfig(gopts.extended); err != nil { if vsscfg, err = fs.ParseVSSConfig(gopts.Extended); err != nil {
return err return err
} }
} }
@@ -492,47 +508,46 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
return err return err
} }
targets, err := collectTargets(opts, args) success := true
targets, err := collectTargets(opts, args, printer.E, term.InputRaw())
if err != nil { if err != nil {
return err if errors.Is(err, ErrInvalidSourceData) {
success = false
} else {
return err
}
} }
timeStamp := time.Now() timeStamp := time.Now()
backupStart := timeStamp backupStart := timeStamp
if opts.TimeStamp != "" { if opts.TimeStamp != "" {
timeStamp, err = time.ParseInLocation(TimeFormat, opts.TimeStamp, time.Local) timeStamp, err = time.ParseInLocation(global.TimeFormat, opts.TimeStamp, time.Local)
if err != nil { if err != nil {
return errors.Fatalf("error in time option: %v\n", err) return errors.Fatalf("error in time option: %v", err)
} }
} }
if gopts.verbosity >= 2 && !gopts.JSON { if gopts.Verbosity >= 2 && !gopts.JSON {
Verbosef("open repository\n") printer.P("open repository")
} }
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, opts.DryRun) ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, opts.DryRun, printer)
if err != nil { if err != nil {
return err return err
} }
defer unlock() defer unlock()
var progressPrinter backup.ProgressPrinter progressReporter := backup.NewProgress(printer,
if gopts.JSON { ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
progressPrinter = backup.NewJSONProgress(term, gopts.verbosity)
} else {
progressPrinter = backup.NewTextProgress(term, gopts.verbosity)
}
progressReporter := backup.NewProgress(progressPrinter,
calculateProgressInterval(!gopts.Quiet, gopts.JSON))
defer progressReporter.Done() defer progressReporter.Done()
// rejectByNameFuncs collect functions that can reject items from the backup based on path only // rejectByNameFuncs collect functions that can reject items from the backup based on path only
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo) rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, printer.E)
if err != nil { if err != nil {
return err return err
} }
var parentSnapshot *restic.Snapshot var parentSnapshot *data.Snapshot
if !opts.Stdin { if !opts.Stdin {
parentSnapshot, err = findParentSnapshot(ctx, repo, opts, targets, timeStamp) parentSnapshot, err = findParentSnapshot(ctx, repo, opts, targets, timeStamp)
if err != nil { if err != nil {
@@ -541,19 +556,18 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
if !gopts.JSON { if !gopts.JSON {
if parentSnapshot != nil { if parentSnapshot != nil {
progressPrinter.P("using parent snapshot %v\n", parentSnapshot.ID().Str()) printer.P("using parent snapshot %v\n", parentSnapshot.ID().Str())
} else { } else {
progressPrinter.P("no parent snapshot found, will read all files\n") printer.P("no parent snapshot found, will read all files\n")
} }
} }
} }
if !gopts.JSON { if !gopts.JSON {
progressPrinter.V("load index files") printer.V("load index files")
} }
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term) err = repo.LoadIndex(ctx, printer)
err = repo.LoadIndex(ctx, bar)
if err != nil { if err != nil {
return err return err
} }
@@ -570,7 +584,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
messageHandler := func(msg string, args ...interface{}) { messageHandler := func(msg string, args ...interface{}) {
if !gopts.JSON { if !gopts.JSON {
progressPrinter.P(msg, args...) printer.P(msg, args...)
} }
} }
@@ -581,12 +595,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
if opts.Stdin || opts.StdinCommand { if opts.Stdin || opts.StdinCommand {
if !gopts.JSON { if !gopts.JSON {
progressPrinter.V("read data from stdin") printer.V("read data from stdin")
} }
filename := path.Join("/", opts.StdinFilename) filename := path.Join("/", opts.StdinFilename)
var source io.ReadCloser = os.Stdin source := term.InputRaw()
if opts.StdinCommand { if opts.StdinCommand {
source, err = fs.NewCommandReader(ctx, args, globalOptions.stderr) source, err = fs.NewCommandReader(ctx, args, printer.E)
if err != nil { if err != nil {
return err return err
} }
@@ -606,7 +620,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
} }
// rejectFuncs collect functions that can reject items from the backup based on path and file info // rejectFuncs collect functions that can reject items from the backup based on path and file info
rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS) rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS, printer.E)
if err != nil { if err != nil {
return err return err
} }
@@ -622,11 +636,11 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
sc := archiver.NewScanner(targetFS) sc := archiver.NewScanner(targetFS)
sc.SelectByName = selectByNameFilter sc.SelectByName = selectByNameFilter
sc.Select = selectFilter sc.Select = selectFilter
sc.Error = progressPrinter.ScannerError sc.Error = printer.ScannerError
sc.Result = progressReporter.ReportTotal sc.Result = progressReporter.ReportTotal
if !gopts.JSON { if !gopts.JSON {
progressPrinter.V("start scan on %v", targets) printer.V("start scan on %v", targets)
} }
wg.Go(func() error { return sc.Scan(cancelCtx, targets) }) wg.Go(func() error { return sc.Scan(cancelCtx, targets) })
} }
@@ -635,7 +649,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
arch.SelectByName = selectByNameFilter arch.SelectByName = selectByNameFilter
arch.Select = selectFilter arch.Select = selectFilter
arch.WithAtime = opts.WithAtime arch.WithAtime = opts.WithAtime
success := true
arch.Error = func(item string, err error) error { arch.Error = func(item string, err error) error {
success = false success = false
reterr := progressReporter.Error(item, err) reterr := progressReporter.Error(item, err)
@@ -666,12 +680,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
Time: timeStamp, Time: timeStamp,
Hostname: opts.Host, Hostname: opts.Host,
ParentSnapshot: parentSnapshot, ParentSnapshot: parentSnapshot,
ProgramVersion: "restic " + version, ProgramVersion: "restic " + global.Version,
SkipIfUnchanged: opts.SkipIfUnchanged, SkipIfUnchanged: opts.SkipIfUnchanged,
} }
if !gopts.JSON { if !gopts.JSON {
progressPrinter.V("start backup on %v", targets) printer.V("start backup on %v", targets)
} }
_, id, summary, err := arch.Snapshot(ctx, targets, snapshotOpts) _, id, summary, err := arch.Snapshot(ctx, targets, snapshotOpts)
+48 -44
View File
@@ -3,33 +3,34 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"testing" "testing"
"time" "time"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/termstatus"
) )
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) error { func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) error {
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { return withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
t.Logf("backing up %v in %v", target, dir) t.Logf("backing up %v in %v", target, dir)
if dir != "" { if dir != "" {
cleanup := rtest.Chdir(t, dir) cleanup := rtest.Chdir(t, dir)
defer cleanup() defer cleanup()
} }
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true} opts.GroupBy = data.SnapshotGroupByOptions{Host: true, Path: true}
return runBackup(ctx, opts, gopts, term, target) return runBackup(ctx, opts, gopts, gopts.Term, target)
}) })
} }
func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) { func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) {
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts) err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
rtest.Assert(t, err == nil, "Error while backing up: %v", err) rtest.Assert(t, err == nil, "Error while backing up: %v", err)
} }
@@ -56,13 +57,13 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
testListSnapshots(t, env.gopts, 1) testListSnapshots(t, env.gopts, 1)
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
stat1 := dirStats(env.repo) stat1 := dirStats(t, env.repo)
// second backup, implicit incremental // second backup, implicit incremental
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
snapshotIDs := testListSnapshots(t, env.gopts, 2) snapshotIDs := testListSnapshots(t, env.gopts, 2)
stat2 := dirStats(env.repo) stat2 := dirStats(t, env.repo)
if stat2.size > stat1.size+stat1.size/10 { if stat2.size > stat1.size+stat1.size/10 {
t.Error("repository size has grown by more than 10 percent") t.Error("repository size has grown by more than 10 percent")
} }
@@ -74,7 +75,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
snapshotIDs = testListSnapshots(t, env.gopts, 3) snapshotIDs = testListSnapshots(t, env.gopts, 3)
stat3 := dirStats(env.repo) stat3 := dirStats(t, env.repo)
if stat3.size > stat1.size+stat1.size/10 { if stat3.size > stat1.size+stat1.size/10 {
t.Error("repository size has grown by more than 10 percent") t.Error("repository size has grown by more than 10 percent")
} }
@@ -85,7 +86,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i)) restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir) t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
testRunRestore(t, env.gopts, restoredir, snapshotID.String()+":"+toPathInSnapshot(filepath.Dir(env.testdata))) testRunRestore(t, env.gopts, restoredir, snapshotID.String()+":"+toPathInSnapshot(filepath.Dir(env.testdata)))
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata")) diff := directoriesContentsDiff(t, env.testdata, filepath.Join(restoredir, "testdata"))
rtest.Assert(t, diff == "", "directories are not equal: %v", diff) rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
} }
@@ -218,41 +219,41 @@ func TestDryRunBackup(t *testing.T) {
// dry run before first backup // dry run before first backup
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts) testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
snapshotIDs := testListSnapshots(t, env.gopts, 0) snapshotIDs := testListSnapshots(t, env.gopts, 0)
packIDs := testRunList(t, "packs", env.gopts) packIDs := testRunList(t, env.gopts, "packs")
rtest.Assert(t, len(packIDs) == 0, rtest.Assert(t, len(packIDs) == 0,
"expected no data, got %v", snapshotIDs) "expected no data, got %v", snapshotIDs)
indexIDs := testRunList(t, "index", env.gopts) indexIDs := testRunList(t, env.gopts, "index")
rtest.Assert(t, len(indexIDs) == 0, rtest.Assert(t, len(indexIDs) == 0,
"expected no index, got %v", snapshotIDs) "expected no index, got %v", snapshotIDs)
// first backup // first backup
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
snapshotIDs = testListSnapshots(t, env.gopts, 1) snapshotIDs = testListSnapshots(t, env.gopts, 1)
packIDs = testRunList(t, "packs", env.gopts) packIDs = testRunList(t, env.gopts, "packs")
indexIDs = testRunList(t, "index", env.gopts) indexIDs = testRunList(t, env.gopts, "index")
// dry run between backups // dry run between backups
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts) testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
snapshotIDsAfter := testListSnapshots(t, env.gopts, 1) snapshotIDsAfter := testListSnapshots(t, env.gopts, 1)
rtest.Equals(t, snapshotIDs, snapshotIDsAfter) rtest.Equals(t, snapshotIDs, snapshotIDsAfter)
dataIDsAfter := testRunList(t, "packs", env.gopts) dataIDsAfter := testRunList(t, env.gopts, "packs")
rtest.Equals(t, packIDs, dataIDsAfter) rtest.Equals(t, packIDs, dataIDsAfter)
indexIDsAfter := testRunList(t, "index", env.gopts) indexIDsAfter := testRunList(t, env.gopts, "index")
rtest.Equals(t, indexIDs, indexIDsAfter) rtest.Equals(t, indexIDs, indexIDsAfter)
// second backup, implicit incremental // second backup, implicit incremental
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
snapshotIDs = testListSnapshots(t, env.gopts, 2) snapshotIDs = testListSnapshots(t, env.gopts, 2)
packIDs = testRunList(t, "packs", env.gopts) packIDs = testRunList(t, env.gopts, "packs")
indexIDs = testRunList(t, "index", env.gopts) indexIDs = testRunList(t, env.gopts, "index")
// another dry run // another dry run
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts) testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
snapshotIDsAfter = testListSnapshots(t, env.gopts, 2) snapshotIDsAfter = testListSnapshots(t, env.gopts, 2)
rtest.Equals(t, snapshotIDs, snapshotIDsAfter) rtest.Equals(t, snapshotIDs, snapshotIDsAfter)
dataIDsAfter = testRunList(t, "packs", env.gopts) dataIDsAfter = testRunList(t, env.gopts, "packs")
rtest.Equals(t, packIDs, dataIDsAfter) rtest.Equals(t, packIDs, dataIDsAfter)
indexIDsAfter = testRunList(t, "index", env.gopts) indexIDsAfter = testRunList(t, env.gopts, "index")
rtest.Equals(t, indexIDs, indexIDsAfter) rtest.Equals(t, indexIDs, indexIDsAfter)
} }
@@ -262,22 +263,27 @@ func TestBackupNonExistingFile(t *testing.T) {
testSetupBackupData(t, env) testSetupBackupData(t, env)
_ = withRestoreGlobalOptions(func() error { p := filepath.Join(env.testdata, "0", "0", "9")
globalOptions.stderr = io.Discard dirs := []string{
filepath.Join(p, "0"),
filepath.Join(p, "1"),
filepath.Join(p, "nonexisting"),
filepath.Join(p, "5"),
}
p := filepath.Join(env.testdata, "0", "0", "9") opts := BackupOptions{}
dirs := []string{
filepath.Join(p, "0"),
filepath.Join(p, "1"),
filepath.Join(p, "nonexisting"),
filepath.Join(p, "5"),
}
opts := BackupOptions{} // mix of existing and non-existing files
err := testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts)
testRunBackup(t, "", dirs, opts, env.gopts) rtest.Assert(t, err != nil, "expected error for non-existing file")
return nil rtest.Assert(t, errors.Is(err, ErrInvalidSourceData), "expected ErrInvalidSourceData; got %v", err)
}) // only non-existing file
dirs = []string{
filepath.Join(p, "nonexisting"),
}
err = testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts)
rtest.Assert(t, err != nil, "expected error for non-existing file")
rtest.Assert(t, errors.Is(err, ErrNoSourceData), "expected ErrNoSourceData; got %v", err)
} }
func TestBackupSelfHealing(t *testing.T) { func TestBackupSelfHealing(t *testing.T) {
@@ -438,13 +444,13 @@ func TestIncrementalBackup(t *testing.T) {
testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunBackup(t, "", []string{datadir}, opts, env.gopts)
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
stat1 := dirStats(env.repo) stat1 := dirStats(t, env.repo)
rtest.OK(t, appendRandomData(testfile, incrementalSecondWrite)) rtest.OK(t, appendRandomData(testfile, incrementalSecondWrite))
testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunBackup(t, "", []string{datadir}, opts, env.gopts)
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
stat2 := dirStats(env.repo) stat2 := dirStats(t, env.repo)
if stat2.size-stat1.size > incrementalFirstWrite { if stat2.size-stat1.size > incrementalFirstWrite {
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite) t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
} }
@@ -454,14 +460,13 @@ func TestIncrementalBackup(t *testing.T) {
testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunBackup(t, "", []string{datadir}, opts, env.gopts)
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
stat3 := dirStats(env.repo) stat3 := dirStats(t, env.repo)
if stat3.size-stat2.size > incrementalFirstWrite { if stat3.size-stat2.size > incrementalFirstWrite {
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite) t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
} }
t.Logf("repository grown by %d bytes", stat3.size-stat2.size) t.Logf("repository grown by %d bytes", stat3.size-stat2.size)
} }
// nolint: staticcheck // false positive nil pointer dereference check
func TestBackupTags(t *testing.T) { func TestBackupTags(t *testing.T) {
env, cleanup := withTestEnvironment(t) env, cleanup := withTestEnvironment(t)
defer cleanup() defer cleanup()
@@ -481,7 +486,7 @@ func TestBackupTags(t *testing.T) {
"expected no tags, got %v", newest.Tags) "expected no tags, got %v", newest.Tags)
parent := newest parent := newest
opts.Tags = restic.TagLists{[]string{"NL"}} opts.Tags = data.TagLists{[]string{"NL"}}
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
newest, _ = testRunSnapshots(t, env.gopts) newest, _ = testRunSnapshots(t, env.gopts)
@@ -497,7 +502,6 @@ func TestBackupTags(t *testing.T) {
"expected parent to be %v, got %v", parent.ID, newest.Parent) "expected parent to be %v, got %v", parent.ID, newest.Parent)
} }
// nolint: staticcheck // false positive nil pointer dereference check
func TestBackupProgramVersion(t *testing.T) { func TestBackupProgramVersion(t *testing.T) {
env, cleanup := withTestEnvironment(t) env, cleanup := withTestEnvironment(t)
defer cleanup() defer cleanup()
@@ -509,7 +513,7 @@ func TestBackupProgramVersion(t *testing.T) {
if newest == nil { if newest == nil {
t.Fatal("expected a backup, got nil") t.Fatal("expected a backup, got nil")
} }
resticVersion := "restic " + version resticVersion := "restic " + global.Version
rtest.Assert(t, newest.ProgramVersion == resticVersion, rtest.Assert(t, newest.ProgramVersion == resticVersion,
"expected %v, got %v", resticVersion, newest.ProgramVersion) "expected %v, got %v", resticVersion, newest.ProgramVersion)
} }
@@ -567,7 +571,7 @@ func TestHardLink(t *testing.T) {
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i)) restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir) t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
testRunRestore(t, env.gopts, restoredir, snapshotID.String()) testRunRestore(t, env.gopts, restoredir, snapshotID.String())
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata")) diff := directoriesContentsDiff(t, env.testdata, filepath.Join(restoredir, "testdata"))
rtest.Assert(t, diff == "", "directories are not equal %v", diff) rtest.Assert(t, diff == "", "directories are not equal %v", diff)
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata")) linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
@@ -703,7 +707,7 @@ func TestBackupEmptyPassword(t *testing.T) {
env, cleanup := withTestEnvironment(t) env, cleanup := withTestEnvironment(t)
defer cleanup() defer cleanup()
env.gopts.password = "" env.gopts.Password = ""
env.gopts.InsecureNoPassword = true env.gopts.InsecureNoPassword = true
testSetupBackupData(t, env) testSetupBackupData(t, env)
+27 -1
View File
@@ -10,6 +10,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/restic/restic/internal/errors"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
@@ -67,10 +68,35 @@ func TestCollectTargets(t *testing.T) {
FilesFromRaw: []string{f3.Name()}, FilesFromRaw: []string{f3.Name()},
} }
targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}) targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}, t.Logf, nil)
rtest.OK(t, err) rtest.OK(t, err)
sort.Strings(targets) sort.Strings(targets)
rtest.Equals(t, expect, targets) rtest.Equals(t, expect, targets)
_, err = collectTargets(opts, []string{filepath.Join(dir, "cmdline arg"), filepath.Join(dir, "non-existing-file")}, t.Logf, nil)
rtest.Assert(t, err == ErrInvalidSourceData, "expected error when not all targets exist")
}
func TestFilterExistingUnreadable(t *testing.T) {
dir := rtest.TempDir(t)
existing := filepath.Join(dir, "existing")
rtest.OK(t, os.Mkdir(existing, 0755))
file := filepath.Join(dir, "file")
rtest.OK(t, os.WriteFile(file, []byte("x"), 0600))
// Regression test for #5667. A target whose Lstat fails with an error other
// than ErrNotExist must be skipped (ENOTDIR on unix, NUL byte everywhere).
for _, unreadable := range []string{filepath.Join(file, "child"), "invalid\x00path"} {
result, err := filterExisting([]string{unreadable}, t.Logf)
rtest.Assert(t, errors.Is(err, ErrNoSourceData), "input %q: expected ErrNoSourceData; got %v", unreadable, err)
rtest.Assert(t, len(result) == 0, "input %q: expected no targets; got %v", unreadable, result)
result, err = filterExisting([]string{existing, unreadable}, t.Logf)
rtest.Assert(t, errors.Is(err, ErrInvalidSourceData), "input %q: expected ErrInvalidSourceData; got %v", unreadable, err)
rtest.Equals(t, []string{existing}, result)
}
} }
func TestReadFilenamesRaw(t *testing.T) { func TestReadFilenamesRaw(t *testing.T) {
+12 -9
View File
@@ -10,13 +10,14 @@ import (
"github.com/restic/restic/internal/backend/cache" "github.com/restic/restic/internal/backend/cache"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/table" "github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newCacheCommand() *cobra.Command { func newCacheCommand(globalOptions *global.Options) *cobra.Command {
var opts CacheOptions var opts CacheOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -34,7 +35,7 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
return runCache(opts, globalOptions, args) return runCache(opts, *globalOptions, args, globalOptions.Term)
}, },
} }
@@ -55,7 +56,9 @@ func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.NoSize, "no-size", false, "do not output the size of the cache directories") f.BoolVar(&opts.NoSize, "no-size", false, "do not output the size of the cache directories")
} }
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { func runCache(opts CacheOptions, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
if len(args) > 0 { if len(args) > 0 {
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags") return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
} }
@@ -83,17 +86,17 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
} }
if len(oldDirs) == 0 { if len(oldDirs) == 0 {
Verbosef("no old cache dirs found\n") printer.P("no old cache dirs found")
return nil return nil
} }
Verbosef("remove %d old cache directories\n", len(oldDirs)) printer.P("remove %d old cache directories", len(oldDirs))
for _, item := range oldDirs { for _, item := range oldDirs {
dir := filepath.Join(cachedir, item.Name()) dir := filepath.Join(cachedir, item.Name())
err = os.RemoveAll(dir) err = os.RemoveAll(dir)
if err != nil { if err != nil {
Warnf("unable to remove %v: %v\n", dir, err) printer.E("unable to remove %v: %v", dir, err)
} }
} }
@@ -123,7 +126,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
} }
if len(dirs) == 0 { if len(dirs) == 0 {
Printf("no cache dirs found, basedir is %v\n", cachedir) printer.S("no cache dirs found, basedir is %v", cachedir)
return nil return nil
} }
@@ -159,8 +162,8 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
}) })
} }
_ = tab.Write(globalOptions.stdout) _ = tab.Write(gopts.Term.OutputWriter())
Printf("%d cache dirs in %s\n", len(dirs), cachedir) printer.S("%d cache dirs in %s", len(dirs), cachedir)
return nil return nil
} }
+27 -24
View File
@@ -7,14 +7,17 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
) )
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"} var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
func newCatCommand() *cobra.Command { func newCatCommand(globalOptions *global.Options) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]", Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
Short: "Print internal objects to stdout", Short: "Print internal objects to stdout",
@@ -33,7 +36,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCat(cmd.Context(), globalOptions, args) return runCat(cmd.Context(), *globalOptions, args, globalOptions.Term)
}, },
ValidArgs: catAllowedCmds, ValidArgs: catAllowedCmds,
} }
@@ -63,12 +66,14 @@ func validateCatArgs(args []string) error {
return nil return nil
} }
func runCat(ctx context.Context, gopts GlobalOptions, 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)
if err := validateCatArgs(args); err != nil { if err := validateCatArgs(args); err != nil {
return err return err
} }
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
@@ -80,7 +85,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
if tpe != "masterkey" && tpe != "config" && tpe != "snapshot" && tpe != "tree" { if tpe != "masterkey" && tpe != "config" && tpe != "snapshot" && tpe != "tree" {
id, err = restic.ParseID(args[1]) id, err = restic.ParseID(args[1])
if err != nil { if err != nil {
return errors.Fatalf("unable to parse ID: %v\n", err) return errors.Fatalf("unable to parse ID: %v", err)
} }
} }
@@ -91,7 +96,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
return err return err
} }
Println(string(buf)) printer.S(string(buf))
return nil return nil
case "index": case "index":
buf, err := repo.LoadUnpacked(ctx, restic.IndexFile, id) buf, err := repo.LoadUnpacked(ctx, restic.IndexFile, id)
@@ -99,12 +104,12 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
return err return err
} }
Println(string(buf)) printer.S(string(buf))
return nil return nil
case "snapshot": case "snapshot":
sn, _, err := restic.FindSnapshot(ctx, repo, repo, args[1]) sn, _, err := data.FindSnapshot(ctx, repo, repo, args[1])
if err != nil { if err != nil {
return errors.Fatalf("could not find snapshot: %v\n", err) return errors.Fatalf("could not find snapshot: %v", err)
} }
buf, err := json.MarshalIndent(sn, "", " ") buf, err := json.MarshalIndent(sn, "", " ")
@@ -112,7 +117,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
return err return err
} }
Println(string(buf)) printer.S(string(buf))
return nil return nil
case "key": case "key":
key, err := repository.LoadKey(ctx, repo, id) key, err := repository.LoadKey(ctx, repo, id)
@@ -125,7 +130,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
return err return err
} }
Println(string(buf)) printer.S(string(buf))
return nil return nil
case "masterkey": case "masterkey":
buf, err := json.MarshalIndent(repo.Key(), "", " ") buf, err := json.MarshalIndent(repo.Key(), "", " ")
@@ -133,7 +138,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
return err return err
} }
Println(string(buf)) printer.S(string(buf))
return nil return nil
case "lock": case "lock":
lock, err := restic.LoadLock(ctx, repo, id) lock, err := restic.LoadLock(ctx, repo, id)
@@ -146,7 +151,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
return err return err
} }
Println(string(buf)) printer.S(string(buf))
return nil return nil
case "pack": case "pack":
@@ -158,15 +163,14 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
hash := restic.Hash(buf) hash := restic.Hash(buf)
if !hash.Equal(id) { if !hash.Equal(id) {
Warnf("Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String()) printer.E("Warning: hash of data does not match ID, want\n %v\ngot:\n %v", id.String(), hash.String())
} }
_, err = globalOptions.stdout.Write(buf) _, err = term.OutputRaw().Write(buf)
return err return err
case "blob": case "blob":
bar := newIndexProgress(gopts.Quiet, gopts.JSON) err = repo.LoadIndex(ctx, printer)
err = repo.LoadIndex(ctx, bar)
if err != nil { if err != nil {
return err return err
} }
@@ -181,25 +185,24 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
return err return err
} }
_, err = globalOptions.stdout.Write(buf) _, err = term.OutputRaw().Write(buf)
return err return err
} }
return errors.Fatal("blob not found") return errors.Fatal("blob not found")
case "tree": case "tree":
sn, subfolder, err := restic.FindSnapshot(ctx, repo, repo, args[1]) sn, subfolder, err := data.FindSnapshot(ctx, repo, repo, args[1])
if err != nil { if err != nil {
return errors.Fatalf("could not find snapshot: %v\n", err) return errors.Fatalf("could not find snapshot: %v", err)
} }
bar := newIndexProgress(gopts.Quiet, gopts.JSON) err = repo.LoadIndex(ctx, printer)
err = repo.LoadIndex(ctx, bar)
if err != nil { if err != nil {
return err return err
} }
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subfolder) sn.Tree, err = data.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
if err != nil { if err != nil {
return err return err
} }
@@ -208,7 +211,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
if err != nil { if err != nil {
return err return err
} }
_, err = globalOptions.stdout.Write(buf) _, err = term.OutputRaw().Write(buf)
return err return err
default: default:
+104 -69
View File
@@ -15,25 +15,30 @@ import (
"github.com/restic/restic/internal/backend/cache" "github.com/restic/restic/internal/backend/cache"
"github.com/restic/restic/internal/checker" "github.com/restic/restic/internal/checker"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/ui/termstatus"
) )
func newCheckCommand() *cobra.Command { func newCheckCommand(globalOptions *global.Options) *cobra.Command {
var opts CheckOptions var opts CheckOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "check [flags]", Use: "check [flags]",
Short: "Check the repository for errors", Short: "Check the repository for errors",
Long: ` Long: `
The "check" command tests the repository for errors and reports any errors it The "check" command tests the repository for errors and reports any errors it
finds. It can also be used to read all data and therefore simulate a restore. finds.
By default, the "check" command will always load all data directly from the By default, check verifies the structural consistency and integrity of
repository and not use a local cache. snapshots, trees and pack files. To also verify the integrity of the actual
backed-up data, use the --read-data or --read-data-subset flags.
By default, check creates a new temporary cache directory to verify data.
To reuse the existing cache, use the --with-cache flag.
EXIT STATUS EXIT STATUS
=========== ===========
@@ -47,14 +52,13 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() finalizeSnapshotFilter(&opts.SnapshotFilter)
defer cancel() summary, err := runCheck(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
summary, err := runCheck(cmd.Context(), opts, globalOptions, args, term)
if globalOptions.JSON { if globalOptions.JSON {
if err != nil && summary.NumErrors == 0 { if err != nil && summary.NumErrors == 0 {
summary.NumErrors = 1 summary.NumErrors = 1
} }
term.Print(ui.ToJSONString(summary)) globalOptions.Term.Print(ui.ToJSONString(summary))
} }
return err return err
}, },
@@ -73,6 +77,7 @@ type CheckOptions struct {
ReadDataSubset string ReadDataSubset string
CheckUnused bool CheckUnused bool
WithCache bool WithCache bool
data.SnapshotFilter
} }
func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) { func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
@@ -86,6 +91,7 @@ func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
panic(err) panic(err)
} }
f.BoolVar(&opts.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository") f.BoolVar(&opts.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository")
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
} }
func checkFlags(opts CheckOptions) error { func checkFlags(opts CheckOptions) error {
@@ -173,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 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 // - 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 // - by default, we use a cache in a temporary directory that is deleted after the check
func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress.Printer) (cleanup func()) { func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progress.Printer) (cleanup func()) {
cleanup = func() {} cleanup = func() {}
if opts.WithCache { if opts.WithCache {
// use the default cache, no setup needed // use the default cache, no setup needed
@@ -194,7 +200,7 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
// use a cache in a temporary directory // use a cache in a temporary directory
err := os.MkdirAll(cachedir, 0755) err := os.MkdirAll(cachedir, 0755)
if err != nil { if err != nil {
Warnf("unable to create cache directory %s, disabling cache: %v\n", cachedir, err) printer.E("unable to create cache directory %s, disabling cache: %v", cachedir, err)
gopts.NoCache = true gopts.NoCache = true
return cleanup return cleanup
} }
@@ -220,15 +226,12 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
return cleanup return cleanup
} }
func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args []string, term *termstatus.Terminal) (checkSummary, error) { func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args []string, term ui.Terminal) (checkSummary, error) {
summary := checkSummary{MessageType: "summary"} summary := checkSummary{MessageType: "summary"}
if len(args) != 0 {
return summary, errors.Fatal("the check command expects no arguments, only options - please see `restic help check` for usage and flags")
}
var printer progress.Printer var printer progress.Printer
if !gopts.JSON { if !gopts.JSON {
printer = newTerminalProgressPrinter(gopts.verbosity, term) printer = ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
} else { } else {
printer = newJSONErrorPrinter(term) printer = newJSONErrorPrinter(term)
} }
@@ -239,32 +242,38 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
if !gopts.NoLock { if !gopts.NoLock {
printer.P("create exclusive lock for repository\n") printer.P("create exclusive lock for repository\n")
} }
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, gopts.NoLock) ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return summary, err return summary, err
} }
defer unlock() defer unlock()
chkr := checker.New(repo, opts.CheckUnused) chkr := checker.New(repo, opts.CheckUnused)
err = chkr.LoadSnapshots(ctx) err = chkr.LoadSnapshots(ctx, &opts.SnapshotFilter, args)
if err != nil { if err != nil {
return summary, err return summary, err
} }
printer.P("load indexes\n") printer.P("load indexes\n")
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term) hints, errs := chkr.LoadIndex(ctx, printer)
hints, errs := chkr.LoadIndex(ctx, bar)
if ctx.Err() != nil { if ctx.Err() != nil {
return summary, ctx.Err() return summary, ctx.Err()
} }
errorsFound := false errorsFound := false
salvagePacks := restic.NewIDSet()
for _, hint := range hints { for _, hint := range hints {
switch hint.(type) { switch hint := hint.(type) {
case *checker.ErrDuplicatePacks: case *repository.ErrIncompletePackEntry:
printer.E("%s", hint.Error())
salvagePacks.Insert(hint.PackID)
errorsFound = true
summary.NumErrors++
case *repository.ErrDuplicatePacks:
printer.S("%s", hint.Error()) printer.S("%s", hint.Error())
summary.HintRepairIndex = true summary.HintRepairIndex = true
case *checker.ErrMixedPack: case *repository.ErrMixedPack:
printer.S("%s", hint.Error()) printer.S("%s", hint.Error())
summary.HintPrune = true summary.HintPrune = true
default: default:
@@ -293,13 +302,12 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
orphanedPacks := 0 orphanedPacks := 0
errChan := make(chan error) errChan := make(chan error)
salvagePacks := restic.NewIDSet()
printer.P("check all packs\n") printer.P("check all packs\n")
go chkr.Packs(ctx, errChan) go chkr.Packs(ctx, errChan)
for err := range errChan { for err := range errChan {
var packErr *checker.PackError var packErr *repository.PackError
if errors.As(err, &packErr) { if errors.As(err, &packErr) {
if packErr.Orphaned { if packErr.Orphaned {
orphanedPacks++ orphanedPacks++
@@ -363,6 +371,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
return summary, ctx.Err() return summary, ctx.Err()
} }
// the following block only used for tests
if opts.CheckUnused { if opts.CheckUnused {
unused, err := chkr.UnusedBlobs(ctx) unused, err := chkr.UnusedBlobs(ctx)
if err != nil { if err != nil {
@@ -374,12 +383,16 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
} }
} }
doReadData := func(packs map[restic.ID]int64) { readDataFilter, err := buildPacksFilter(opts, printer, chkr.IsFiltered())
if err != nil {
return summary, err
}
if readDataFilter != nil {
p := printer.NewCounter("packs") p := printer.NewCounter("packs")
p.SetMax(uint64(len(packs)))
errChan := make(chan error) errChan := make(chan error)
go chkr.ReadPacks(ctx, packs, p, errChan) go chkr.ReadPacks(ctx, readDataFilter, p, errChan)
for err := range errChan { for err := range errChan {
errorsFound = true errorsFound = true
@@ -392,47 +405,6 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
p.Done() p.Done()
} }
switch {
case opts.ReadData:
printer.P("read all data\n")
doReadData(selectPacksByBucket(chkr.GetPacks(), 1, 1))
case opts.ReadDataSubset != "":
var packs map[restic.ID]int64
dataSubset, err := stringToIntSlice(opts.ReadDataSubset)
if err == nil {
bucket := dataSubset[0]
totalBuckets := dataSubset[1]
packs = selectPacksByBucket(chkr.GetPacks(), bucket, totalBuckets)
packCount := uint64(len(packs))
printer.P("read group #%d of %d data packs (out of total %d packs in %d groups)\n", bucket, packCount, chkr.CountPacks(), totalBuckets)
} else if strings.HasSuffix(opts.ReadDataSubset, "%") {
percentage, err := parsePercentage(opts.ReadDataSubset)
if err == nil {
packs = selectRandomPacksByPercentage(chkr.GetPacks(), percentage)
printer.P("read %.1f%% of data packs\n", percentage)
}
} else {
repoSize := int64(0)
allPacks := chkr.GetPacks()
for _, size := range allPacks {
repoSize += size
}
if repoSize == 0 {
return summary, errors.Fatal("Cannot read from a repository having size 0")
}
subsetSize, _ := ui.ParseBytes(opts.ReadDataSubset)
if subsetSize > repoSize {
subsetSize = repoSize
}
packs = selectRandomPacksByFileSize(chkr.GetPacks(), subsetSize, repoSize)
printer.P("read %d bytes of data packs\n", subsetSize)
}
if packs == nil {
return summary, errors.Fatal("internal error: failed to select packs to check")
}
doReadData(packs)
}
if len(salvagePacks) > 0 { if len(salvagePacks) > 0 {
printer.E("\nThe repository contains damaged pack 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("\nThe repository contains damaged pack 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")
for id := range salvagePacks { for id := range salvagePacks {
@@ -456,6 +428,64 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
return summary, nil return summary, nil
} }
func buildPacksFilter(opts CheckOptions, printer progress.Printer,
filteredStatus bool) (func(packs map[restic.ID]int64) map[restic.ID]int64, error) {
typeData := ""
if filteredStatus {
typeData = "filtered "
}
switch {
case opts.ReadData:
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
printer.P("read all %sdata", typeData)
return packs
}, nil
case opts.ReadDataSubset != "":
dataSubset, err := stringToIntSlice(opts.ReadDataSubset)
if err == nil {
bucket := dataSubset[0]
totalBuckets := dataSubset[1]
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
packCount := uint64(len(packs))
packs = selectPacksByBucket(packs, bucket, totalBuckets)
printer.P("read group #%d of %d %sdata packs (out of total %d packs in %d groups", bucket, len(packs), typeData, packCount, totalBuckets)
return packs
}, nil
} else if strings.HasSuffix(opts.ReadDataSubset, "%") {
percentage, err := parsePercentage(opts.ReadDataSubset)
if err != nil {
return nil, err
}
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
printer.P("read %.1f%% of %spackfiles", percentage, typeData)
return selectRandomPacksByPercentage(packs, percentage)
}, nil
}
repoSize := int64(0)
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
for _, size := range packs {
repoSize += size
}
subsetSize, _ := ui.ParseBytes(opts.ReadDataSubset)
if subsetSize > repoSize {
subsetSize = repoSize
}
if repoSize > 0 {
packs = selectRandomPacksByFileSize(packs, subsetSize, repoSize)
}
percentage := float64(subsetSize) / float64(repoSize) * 100.0
if repoSize == 0 {
percentage = 100
}
printer.P("read %d bytes (%.1f%%) of %sdata packs\n", subsetSize, percentage, typeData)
return packs
}, nil
}
return nil, nil
}
// selectPacksByBucket selects subsets of packs by ranges of buckets. // selectPacksByBucket selects subsets of packs by ranges of buckets.
func selectPacksByBucket(allPacks map[restic.ID]int64, bucket, totalBuckets uint) map[restic.ID]int64 { func selectPacksByBucket(allPacks map[restic.ID]int64, bucket, totalBuckets uint) map[restic.ID]int64 {
packs := make(map[restic.ID]int64) packs := make(map[restic.ID]int64)
@@ -527,6 +557,10 @@ func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter {
return nil return nil
} }
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) *progress.Counter {
return nil
}
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) { func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
status := checkError{ status := checkError{
MessageType: "error", MessageType: "error",
@@ -536,5 +570,6 @@ func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
} }
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {} func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {} func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {} func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {} func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
+73 -11
View File
@@ -1,39 +1,101 @@
package main package main
import ( import (
"bytes"
"context" "context"
"strings"
"testing" "testing"
"github.com/restic/restic/internal/global"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/termstatus"
) )
func testRunCheck(t testing.TB, gopts GlobalOptions) { func testRunCheck(t testing.TB, gopts global.Options) {
t.Helper() t.Helper()
output, err := testRunCheckOutput(gopts, true) output, err := testRunCheckOutput(t, gopts, true)
if err != nil { if err != nil {
t.Error(output) t.Error(output)
t.Fatalf("unexpected error: %+v", err) t.Fatalf("unexpected error: %+v", err)
} }
} }
func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) { func testRunCheckMustFail(t testing.TB, gopts global.Options) {
t.Helper() t.Helper()
_, err := testRunCheckOutput(gopts, false) _, err := testRunCheckOutput(t, gopts, false)
rtest.Assert(t, err != nil, "expected non nil error after check of damaged repository") rtest.Assert(t, err != nil, "expected non nil error after check of damaged repository")
} }
func testRunCheckOutput(gopts GlobalOptions, checkUnused bool) (string, error) { func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, error) {
buf := bytes.NewBuffer(nil) buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
gopts.stdout = buf
err := withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
opts := CheckOptions{ opts := CheckOptions{
ReadData: true, ReadData: true,
CheckUnused: checkUnused, CheckUnused: checkUnused,
} }
_, err := runCheck(context.TODO(), opts, gopts, nil, term) _, err := runCheck(context.TODO(), opts, gopts, nil, gopts.Term)
return err return err
}) })
return buf.String(), err return buf.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)
return err
})
return buf.String(), err
}
func TestCheckWithSnaphotFilter(t *testing.T) {
testCases := []struct {
opts CheckOptions
args []string
expectedOutput string
}{
{ // full --read-data, all snapshots
CheckOptions{ReadData: true},
nil,
"4 / 4 packs",
},
{ // full --read-data, all snapshots
CheckOptions{ReadData: true},
nil,
"2 / 2 snapshots",
},
{ // full --read-data, latest snapshot
CheckOptions{ReadData: true},
[]string{"latest"},
"2 / 2 packs",
},
{ // full --read-data, latest snapshot
CheckOptions{ReadData: true},
[]string{"latest"},
"1 / 1 snapshots",
},
{ // --read-data-subset, latest snapshot
CheckOptions{ReadDataSubset: "1%"},
[]string{"latest"},
"1 / 1 packs",
},
{ // --read-data-subset, latest snapshot
CheckOptions{ReadDataSubset: "1%"},
[]string{"latest"},
"filtered",
},
}
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
opts := BackupOptions{}
testRunBackup(t, env.testdata+"/0", []string{"for_cmd_ls"}, opts, env.gopts)
testRunBackup(t, env.testdata+"/0", []string{"0/9"}, opts, env.gopts)
for _, testCase := range testCases {
output, err := testRunCheckOutputWithOpts(t, env.gopts, testCase.opts, testCase.args)
rtest.OK(t, err)
hasOutput := strings.Contains(output, testCase.expectedOutput)
rtest.Assert(t, hasOutput, `expected to find substring %q, but did not find it`, testCase.expectedOutput)
}
}
+3 -2
View File
@@ -9,6 +9,7 @@ import (
"testing" "testing"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/progress"
@@ -202,7 +203,7 @@ func TestPrepareCheckCache(t *testing.T) {
err := os.Remove(tmpDirBase) err := os.Remove(tmpDirBase)
rtest.OK(t, err) rtest.OK(t, err)
} }
gopts := GlobalOptions{CacheDir: tmpDirBase} gopts := global.Options{CacheDir: tmpDirBase}
cleanup := prepareCheckCache(testCase.opts, &gopts, &progress.NoopPrinter{}) cleanup := prepareCheckCache(testCase.opts, &gopts, &progress.NoopPrinter{})
files, err := os.ReadDir(tmpDirBase) files, err := os.ReadDir(tmpDirBase)
rtest.OK(t, err) rtest.OK(t, err)
@@ -232,7 +233,7 @@ func TestPrepareCheckCache(t *testing.T) {
} }
func TestPrepareDefaultCheckCache(t *testing.T) { func TestPrepareDefaultCheckCache(t *testing.T) {
gopts := GlobalOptions{CacheDir: ""} gopts := global.Options{CacheDir: ""}
cleanup := prepareCheckCache(CheckOptions{}, &gopts, &progress.NoopPrinter{}) cleanup := prepareCheckCache(CheckOptions{}, &gopts, &progress.NoopPrinter{})
_, err := os.ReadDir(gopts.CacheDir) _, err := os.ReadDir(gopts.CacheDir)
rtest.OK(t, err) rtest.OK(t, err)
+197 -109
View File
@@ -3,18 +3,24 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"iter"
"sync"
"time"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"golang.org/x/sync/errgroup" "github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newCopyCommand() *cobra.Command { func newCopyCommand(globalOptions *global.Options) *cobra.Command {
var opts CopyOptions var opts CopyOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "copy [flags] [snapshotID ...]", Use: "copy [flags] [snapshotID ...]",
@@ -46,7 +52,8 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCopy(cmd.Context(), opts, globalOptions, args) finalizeSnapshotFilter(&opts.SnapshotFilter)
return runCopy(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
}, },
} }
@@ -56,17 +63,51 @@ Exit status is 12 if the password is incorrect.
// CopyOptions bundles all options for the copy command. // CopyOptions bundles all options for the copy command.
type CopyOptions struct { type CopyOptions struct {
secondaryRepoOptions global.SecondaryRepoOptions
restic.SnapshotFilter data.SnapshotFilter
} }
func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) { func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
opts.secondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from") opts.SecondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from")
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true) initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
} }
func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error { // collectAllSnapshots: select all snapshot trees to be copied
secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination") 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) {
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
srcOriginal := *sn.ID()
if sn.Original != nil {
srcOriginal = *sn.Original
}
if originalSns, ok := dstSnapshotByOriginal[srcOriginal]; ok {
isCopy := false
for _, originalSn := range originalSns {
if similarSnapshots(originalSn, sn) {
printer.V("\n%v", sn)
printer.V("skipping source snapshot %s, was already copied to snapshot %s", sn.ID().Str(), originalSn.ID().Str())
isCopy = true
break
}
}
if isCopy {
continue
}
}
if !yield(sn) {
return
}
}
}
}
func runCopy(ctx context.Context, opts CopyOptions, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
secondaryGopts, isFromRepo, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "destination")
if err != nil { if err != nil {
return err return err
} }
@@ -75,13 +116,13 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
gopts, secondaryGopts = secondaryGopts, gopts gopts, secondaryGopts = secondaryGopts, gopts
} }
ctx, srcRepo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) ctx, srcRepo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
defer unlock() defer unlock()
ctx, dstRepo, unlock, err := openWithAppendLock(ctx, secondaryGopts, false) ctx, dstRepo, unlock, err := openWithAppendLock(ctx, secondaryGopts, false, printer)
if err != nil { if err != nil {
return err return err
} }
@@ -98,18 +139,16 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
} }
debug.Log("Loading source index") debug.Log("Loading source index")
bar := newIndexProgress(gopts.Quiet, gopts.JSON) if err := srcRepo.LoadIndex(ctx, printer); err != nil {
if err := srcRepo.LoadIndex(ctx, bar); err != nil {
return err return err
} }
bar = newIndexProgress(gopts.Quiet, gopts.JSON)
debug.Log("Loading destination index") debug.Log("Loading destination index")
if err := dstRepo.LoadIndex(ctx, bar); err != nil { if err := dstRepo.LoadIndex(ctx, printer); err != nil {
return err return err
} }
dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot) dstSnapshotByOriginal := make(map[restic.ID][]*data.Snapshot)
for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil) { for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil, printer) {
if sn.Original != nil && !sn.Original.IsNull() { if sn.Original != nil && !sn.Original.IsNull() {
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn) dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
} }
@@ -120,53 +159,16 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
return ctx.Err() return ctx.Err()
} }
// remember already processed trees across all snapshots selectedSnapshots := collectAllSnapshots(ctx, opts, srcSnapshotLister, srcRepo, dstSnapshotByOriginal, args, printer)
visitedTrees := restic.NewIDSet()
for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args) { if err := copyTreeBatched(ctx, srcRepo, dstRepo, selectedSnapshots, printer); err != nil {
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields return err
srcOriginal := *sn.ID()
if sn.Original != nil {
srcOriginal = *sn.Original
}
if originalSns, ok := dstSnapshotByOriginal[srcOriginal]; ok {
isCopy := false
for _, originalSn := range originalSns {
if similarSnapshots(originalSn, sn) {
Verboseff("\n%v\n", sn)
Verboseff("skipping source snapshot %s, was already copied to snapshot %s\n", sn.ID().Str(), originalSn.ID().Str())
isCopy = true
break
}
}
if isCopy {
continue
}
}
Verbosef("\n%v\n", sn)
Verbosef(" copy started, this may take a while...\n")
if err := copyTree(ctx, srcRepo, dstRepo, visitedTrees, *sn.Tree, gopts.Quiet); err != nil {
return err
}
debug.Log("tree copied")
// save snapshot
sn.Parent = nil // Parent does not have relevance in the new repo.
// Use Original as a persistent snapshot ID
if sn.Original == nil {
sn.Original = sn.ID()
}
newID, err := restic.SaveSnapshot(ctx, dstRepo, sn)
if err != nil {
return err
}
Verbosef("snapshot %s saved\n", newID.Str())
} }
return ctx.Err() return ctx.Err()
} }
func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool { func similarSnapshots(sna *data.Snapshot, snb *data.Snapshot) bool {
// everything except Parent and Original must match // everything except Parent and Original must match
if !sna.Time.Equal(snb.Time) || !sna.Tree.Equal(*snb.Tree) || sna.Hostname != snb.Hostname || if !sna.Time.Equal(snb.Time) || !sna.Tree.Equal(*snb.Tree) || sna.Hostname != snb.Hostname ||
sna.Username != snb.Username || sna.UID != snb.UID || sna.GID != snb.GID || sna.Username != snb.Username || sna.UID != snb.UID || sna.GID != snb.GID ||
@@ -185,72 +187,158 @@ func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
return true return true
} }
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository, // copyTreeBatched copies multiple snapshots in one go. Snapshots are written after
visitedTrees restic.IDSet, rootTreeID restic.ID, quiet bool) error { // 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 {
wg, wgCtx := errgroup.WithContext(ctx) // remember already processed trees across all snapshots
visitedTrees := srcRepo.NewAssociatedBlobSet()
treeStream := restic.StreamTrees(wgCtx, wg, srcRepo, restic.IDs{rootTreeID}, func(treeID restic.ID) bool { targetSize := uint64(dstRepo.PackSize()) * 100
visited := visitedTrees.Has(treeID) minDuration := 1 * time.Minute
visitedTrees.Insert(treeID)
return visited
}, nil)
copyBlobs := restic.NewBlobSet() // use pull-based iterator to allow iteration in multiple steps
packList := restic.NewIDSet() next, stop := iter.Pull(selectedSnapshots)
defer stop()
enqueue := func(h restic.BlobHandle) { for {
pb := srcRepo.LookupBlob(h.Type, h.ID) var batch []*data.Snapshot
copyBlobs.Insert(h) batchSize := uint64(0)
for _, p := range pb { startTime := time.Now()
packList.Insert(p.PackID)
// 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()
if !ok {
break
}
batch = append(batch, sn)
printer.P("\n%v", sn)
printer.P(" copy started, this may take a while...")
sizeBlobs, err := copyTree(ctx, srcRepo, dstRepo, visitedTrees, *sn.Tree, printer, uploader)
if err != nil {
return err
}
debug.Log("tree copied")
batchSize += sizeBlobs
}
return nil
})
if err != nil {
return err
}
// if no snapshots were processed in this batch, we're done
if len(batch) == 0 {
break
}
// add a newline to separate saved snapshot messages from the other messages
if len(batch) > 1 {
printer.P("")
}
// save all the snapshots
for _, sn := range batch {
err := copySaveSnapshot(ctx, sn, dstRepo, printer)
if err != nil {
return err
}
} }
} }
wg.Go(func() error { return nil
for tree := range treeStream { }
if tree.Error != nil {
return fmt.Errorf("LoadTree(%v) returned error %v", tree.ID.Str(), tree.Error)
}
// Do we already have this tree blob? func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
treeHandle := restic.BlobHandle{ID: tree.ID, Type: restic.TreeBlob} visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer progress.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
if _, ok := dstRepo.LookupBlobSize(treeHandle.Type, treeHandle.ID); !ok {
// copy raw tree bytes to avoid problems if the serialization changes
enqueue(treeHandle)
}
for _, entry := range tree.Nodes { copyBlobs := srcRepo.NewAssociatedBlobSet()
// Recursion into directories is handled by StreamTrees packList := restic.NewIDSet()
// Copy the blobs for this file. var lock sync.Mutex
for _, blobID := range entry.Content {
h := restic.BlobHandle{Type: restic.DataBlob, ID: blobID} enqueue := func(h restic.BlobHandle) {
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok { lock.Lock()
enqueue(h) defer lock.Unlock()
} if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
} pb := srcRepo.LookupBlob(h.Type, h.ID)
copyBlobs.Insert(h)
for _, p := range pb {
packList.Insert(p.PackID)
}
}
}
err := data.StreamTrees(ctx, srcRepo, restic.IDs{rootTreeID}, nil, func(treeID restic.ID) bool {
handle := restic.BlobHandle{ID: treeID, Type: restic.TreeBlob}
visited := visitedTrees.Has(handle)
visitedTrees.Insert(handle)
return visited
}, func(treeID restic.ID, err error, nodes data.TreeNodeIterator) error {
if err != nil {
return fmt.Errorf("LoadTree(%v) returned error %v", treeID.Str(), err)
}
// copy raw tree bytes to avoid problems if the serialization changes
enqueue(restic.BlobHandle{ID: treeID, Type: restic.TreeBlob})
for item := range nodes {
if item.Error != nil {
return item.Error
}
// Recursion into directories is handled by StreamTrees
// Copy the blobs for this file.
for _, blobID := range item.Node.Content {
enqueue(restic.BlobHandle{Type: restic.DataBlob, ID: blobID})
} }
} }
return nil return nil
}) })
err := wg.Wait() if err != nil {
return 0, err
}
sizeBlobs := copyStats(srcRepo, copyBlobs, packList, printer)
bar := printer.NewCounter("packs copied")
err = repository.CopyBlobs(ctx, srcRepo, dstRepo, uploader, packList, copyBlobs, bar, printer.P)
if err != nil {
return 0, errors.Fatalf("%s", err)
}
return sizeBlobs, nil
}
// copyStats: print statistics for the blobs to be copied
func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, packList restic.IDSet, printer progress.Printer) uint64 {
// count and size
countBlobs := 0
sizeBlobs := uint64(0)
for blob := range copyBlobs.Keys() {
for _, blob := range srcRepo.LookupBlob(blob.Type, blob.ID) {
countBlobs++
sizeBlobs += uint64(blob.Length)
break
}
}
printer.V(" copy %d blobs with disk size %s in %d packfiles\n",
countBlobs, ui.FormatBytes(uint64(sizeBlobs)), len(packList))
return sizeBlobs
}
func copySaveSnapshot(ctx context.Context, sn *data.Snapshot, dstRepo restic.Repository, printer progress.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 {
sn.Original = sn.ID()
}
newID, err := data.SaveSnapshot(ctx, dstRepo, sn)
if err != nil { if err != nil {
return err return err
} }
printer.P("snapshot %s saved, copied from source snapshot %s", newID.Str(), sn.ID().Str())
bar := newProgressMax(!quiet, uint64(len(packList)), "packs copied")
_, err = repository.Repack(
ctx,
srcRepo,
dstRepo,
packList,
copyBlobs,
bar,
func(msg string, args ...interface{}) { fmt.Printf(msg+"\n", args...) },
)
bar.Done()
if err != nil {
return errors.Fatal(err.Error())
}
return nil return nil
} }
+49 -9
View File
@@ -6,23 +6,28 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
) )
func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) { func testRunCopy(t testing.TB, srcGopts global.Options, dstGopts global.Options) {
gopts := srcGopts gopts := srcGopts
gopts.Repo = dstGopts.Repo gopts.Repo = dstGopts.Repo
gopts.password = dstGopts.password gopts.Password = dstGopts.Password
gopts.InsecureNoPassword = dstGopts.InsecureNoPassword gopts.InsecureNoPassword = dstGopts.InsecureNoPassword
copyOpts := CopyOptions{ copyOpts := CopyOptions{
secondaryRepoOptions: secondaryRepoOptions{ SecondaryRepoOptions: global.SecondaryRepoOptions{
Repo: srcGopts.Repo, Repo: srcGopts.Repo,
password: srcGopts.password, Password: srcGopts.Password,
InsecureNoPassword: srcGopts.InsecureNoPassword, InsecureNoPassword: srcGopts.InsecureNoPassword,
}, },
} }
rtest.OK(t, runCopy(context.TODO(), copyOpts, gopts, nil)) rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runCopy(context.TODO(), copyOpts, gopts, nil, gopts.Term)
}))
} }
func TestCopy(t *testing.T) { func TestCopy(t *testing.T) {
@@ -45,8 +50,8 @@ func TestCopy(t *testing.T) {
copiedSnapshotIDs := testListSnapshots(t, env2.gopts, 3) copiedSnapshotIDs := testListSnapshots(t, env2.gopts, 3)
// Check that the copies size seems reasonable // Check that the copies size seems reasonable
stat := dirStats(env.repo) stat := dirStats(t, env.repo)
stat2 := dirStats(env2.repo) stat2 := dirStats(t, env2.repo)
sizeDiff := int64(stat.size) - int64(stat2.size) sizeDiff := int64(stat.size) - int64(stat2.size)
if sizeDiff < 0 { if sizeDiff < 0 {
sizeDiff = -sizeDiff sizeDiff = -sizeDiff
@@ -69,7 +74,7 @@ func TestCopy(t *testing.T) {
testRunRestore(t, env2.gopts, restoredir, snapshotID.String()) testRunRestore(t, env2.gopts, restoredir, snapshotID.String())
foundMatch := false foundMatch := false
for cmpdir := range origRestores { for cmpdir := range origRestores {
diff := directoriesContentsDiff(restoredir, cmpdir) diff := directoriesContentsDiff(t, restoredir, cmpdir)
if diff == "" { if diff == "" {
delete(origRestores, cmpdir) delete(origRestores, cmpdir)
foundMatch = true foundMatch = true
@@ -80,6 +85,41 @@ func TestCopy(t *testing.T) {
} }
rtest.Assert(t, len(origRestores) == 0, "found not copied snapshots") rtest.Assert(t, len(origRestores) == 0, "found not copied snapshots")
// check that snapshots were properly batched while copying
_, _, countBlobs := testPackAndBlobCounts(t, env.gopts)
countTreePacksDst, countDataPacksDst, countBlobsDst := testPackAndBlobCounts(t, env2.gopts)
rtest.Equals(t, countBlobs, countBlobsDst, "expected blob count in both repos to be equal")
rtest.Equals(t, countTreePacksDst, 1, "expected 1 tree packfile")
rtest.Equals(t, countDataPacksDst, 1, "expected 1 data packfile")
}
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)
_, 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)
rtest.OK(t, err)
rtest.Assert(t, len(blobs) > 0, "a packfile should contain at least one blob")
switch blobs[0].Type {
case restic.TreeBlob:
countTreePacks++
case restic.DataBlob:
countDataPacks++
}
countBlobs += len(blobs)
return nil
}))
return nil
}))
return countTreePacks, countDataPacks, countBlobs
} }
func TestCopyIncremental(t *testing.T) { func TestCopyIncremental(t *testing.T) {
@@ -142,7 +182,7 @@ func TestCopyToEmptyPassword(t *testing.T) {
defer cleanup() defer cleanup()
env2, cleanup2 := withTestEnvironment(t) env2, cleanup2 := withTestEnvironment(t)
defer cleanup2() defer cleanup2()
env2.gopts.password = "" env2.gopts.Password = ""
env2.gopts.InsecureNoPassword = true env2.gopts.InsecureNoPassword = true
testSetupBackupData(t, env) testSetupBackupData(t, env)
+83 -91
View File
@@ -1,5 +1,4 @@
//go:build debug //go:build debug
// +build debug
package main package main
@@ -12,7 +11,6 @@ import (
"io" "io"
"os" "os"
"runtime" "runtime"
"sort"
"sync" "sync"
"time" "time"
@@ -22,32 +20,36 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/restic/restic/internal/crypto" "github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/repository/index" "github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/repository/pack" "github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
) )
func registerDebugCommand(cmd *cobra.Command) { func registerDebugCommand(cmd *cobra.Command, globalOptions *global.Options) {
cmd.AddCommand( cmd.AddCommand(
newDebugCommand(), newDebugCommand(globalOptions),
) )
} }
func newDebugCommand() *cobra.Command { func newDebugCommand(globalOptions *global.Options) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "debug", Use: "debug",
Short: "Debug commands", Short: "Debug commands",
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
} }
cmd.AddCommand(newDebugDumpCommand()) cmd.AddCommand(newDebugDumpCommand(globalOptions))
cmd.AddCommand(newDebugExamineCommand()) cmd.AddCommand(newDebugExamineCommand(globalOptions))
return cmd return cmd
} }
func newDebugDumpCommand() *cobra.Command { func newDebugDumpCommand(globalOptions *global.Options) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dump [indexes|snapshots|all|packs]", Use: "dump [indexes|snapshots|all|packs]",
Short: "Dump data structures", Short: "Dump data structures",
@@ -66,13 +68,13 @@ Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDebugDump(cmd.Context(), globalOptions, args) return runDebugDump(cmd.Context(), *globalOptions, args, globalOptions.Term)
}, },
} }
return cmd return cmd
} }
func newDebugExamineCommand() *cobra.Command { func newDebugExamineCommand(globalOptions *global.Options) *cobra.Command {
var opts DebugExamineOptions var opts DebugExamineOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -80,7 +82,7 @@ func newDebugExamineCommand() *cobra.Command {
Short: "Examine a pack file", Short: "Examine a pack file",
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDebugExamine(cmd.Context(), globalOptions, opts, args) return runDebugExamine(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
}, },
} }
@@ -113,7 +115,7 @@ func prettyPrintJSON(wr io.Writer, item interface{}) error {
} }
func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io.Writer) error { func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io.Writer) error {
return restic.ForAllSnapshots(ctx, repo, repo, nil, func(id restic.ID, snapshot *restic.Snapshot, err error) error { return data.ForAllSnapshots(ctx, repo, repo, nil, func(id restic.ID, snapshot *data.Snapshot, err error) error {
if err != nil { if err != nil {
return err return err
} }
@@ -141,13 +143,13 @@ type Blob struct {
Offset uint `json:"offset"` Offset uint `json:"offset"`
} }
func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer) error { func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer, printer progress.Printer) error {
var m sync.Mutex var m sync.Mutex
return restic.ParallelList(ctx, repo, restic.PackFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error { 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) blobs, _, err := repo.ListPack(ctx, id, size)
if err != nil { if err != nil {
Warnf("error for pack %v: %v\n", id.Str(), err) printer.E("error for pack %v: %v", id.Str(), err)
return nil return nil
} }
@@ -170,9 +172,9 @@ func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer)
}) })
} }
func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer) error { 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 { return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error {
Printf("index_id: %v\n", id) printer.S("index_id: %v", id)
if err != nil { if err != nil {
return err return err
} }
@@ -181,12 +183,14 @@ func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Wr
}) })
} }
func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error { func runDebugDump(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
if len(args) != 1 { if len(args) != 1 {
return errors.Fatal("type not specified") return errors.Fatal("type not specified")
} }
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
@@ -196,20 +200,20 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
switch tpe { switch tpe {
case "indexes": case "indexes":
return dumpIndexes(ctx, repo, globalOptions.stdout) return dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
case "snapshots": case "snapshots":
return debugPrintSnapshots(ctx, repo, globalOptions.stdout) return debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
case "packs": case "packs":
return printPacks(ctx, repo, globalOptions.stdout) return printPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
case "all": case "all":
Printf("snapshots:\n") printer.S("snapshots:")
err := debugPrintSnapshots(ctx, repo, globalOptions.stdout) err := debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
if err != nil { if err != nil {
return err return err
} }
Printf("\nindexes:\n") printer.S("indexes:")
err = dumpIndexes(ctx, repo, globalOptions.stdout) err = dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
if err != nil { if err != nil {
return err return err
} }
@@ -220,11 +224,11 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
} }
} }
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte { func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer progress.Printer) []byte {
if bytewise { if bytewise {
Printf(" trying to repair blob by finding a broken byte\n") printer.S(" trying to repair blob by finding a broken byte")
} else { } else {
Printf(" trying to repair blob with single bit flip\n") printer.S(" trying to repair blob with single bit flip")
} }
ch := make(chan int) ch := make(chan int)
@@ -234,7 +238,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
var found bool var found bool
workers := runtime.GOMAXPROCS(0) workers := runtime.GOMAXPROCS(0)
Printf(" spinning up %d worker functions\n", runtime.GOMAXPROCS(0)) printer.S(" spinning up %d worker functions", runtime.GOMAXPROCS(0))
for i := 0; i < workers; i++ { for i := 0; i < workers; i++ {
wg.Go(func() error { wg.Go(func() error {
// make a local copy of the buffer // make a local copy of the buffer
@@ -248,9 +252,9 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():] nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
plaintext, err := key.Open(plaintext[:0], nonce, plaintext, nil) plaintext, err := key.Open(plaintext[:0], nonce, plaintext, nil)
if err == nil { if err == nil {
Printf("\n") printer.S("")
Printf(" blob could be repaired by XORing byte %v with 0x%02x\n", idx, pattern) printer.S(" blob could be repaired by XORing byte %v with 0x%02x", idx, pattern)
Printf(" hash is %v\n", restic.Hash(plaintext)) printer.S(" hash is %v", restic.Hash(plaintext))
close(done) close(done)
found = true found = true
fixed = plaintext fixed = plaintext
@@ -291,7 +295,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
select { select {
case ch <- i: case ch <- i:
case <-done: case <-done:
Printf(" done after %v\n", time.Since(start)) printer.S(" done after %v", time.Since(start))
return nil return nil
} }
@@ -301,7 +305,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
remaining := len(input) - i remaining := len(input) - i
eta := time.Duration(float64(remaining)/gps) * time.Second eta := time.Duration(float64(remaining)/gps) * time.Second
Printf("\r%d byte of %d done (%.2f%%), %.0f byte per second, ETA %v", 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) i, len(input), float32(i)/float32(len(input))*100, gps, eta)
info = time.Now() info = time.Now()
} }
@@ -314,7 +318,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
} }
if !found { if !found {
Printf("\n blob could not be repaired\n") printer.S("\n blob could not be repaired")
} }
return fixed return fixed
} }
@@ -335,7 +339,7 @@ func decryptUnsigned(k *crypto.Key, buf []byte) []byte {
return out return out
} }
func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, packID restic.ID, list []restic.Blob) error { 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) dec, err := zstd.NewReader(nil)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -347,17 +351,11 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
return err return err
} }
wg, ctx := errgroup.WithContext(ctx) err = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
if opts.ReuploadBlobs {
repo.StartPackUploader(ctx, wg)
}
wg.Go(func() error {
for _, blob := range list { for _, blob := range list {
Printf(" loading blob %v at %v (length %v)\n", blob.ID, blob.Offset, blob.Length) printer.S(" loading blob %v at %v (length %v)", blob.ID, blob.Offset, blob.Length)
if int(blob.Offset+blob.Length) > len(pack) { if int(blob.Offset+blob.Length) > len(pack) {
Warnf("skipping truncated blob\n") printer.E("skipping truncated blob")
continue continue
} }
buf := pack[blob.Offset : blob.Offset+blob.Length] buf := pack[blob.Offset : blob.Offset+blob.Length]
@@ -368,16 +366,16 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
outputPrefix := "" outputPrefix := ""
filePrefix := "" filePrefix := ""
if err != nil { if err != nil {
Warnf("error decrypting blob: %v\n", err) printer.E("error decrypting blob: %v", err)
if opts.TryRepair || opts.RepairByte { if opts.TryRepair || opts.RepairByte {
plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte) plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte, printer)
} }
if plaintext != nil { if plaintext != nil {
outputPrefix = "repaired " outputPrefix = "repaired "
filePrefix = "repaired-" filePrefix = "repaired-"
} else { } else {
plaintext = decryptUnsigned(key, buf) plaintext = decryptUnsigned(key, buf)
err = storePlainBlob(blob.ID, "damaged-", plaintext) err = storePlainBlob(blob.ID, "damaged-", plaintext, printer)
if err != nil { if err != nil {
return err return err
} }
@@ -388,7 +386,7 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
if blob.IsCompressed() { if blob.IsCompressed() {
decompressed, err := dec.DecodeAll(plaintext, nil) decompressed, err := dec.DecodeAll(plaintext, nil)
if err != nil { if err != nil {
Printf(" failed to decompress blob %v\n", blob.ID) printer.S(" failed to decompress blob %v", blob.ID)
} }
if decompressed != nil { if decompressed != nil {
plaintext = decompressed plaintext = decompressed
@@ -398,37 +396,32 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
id := restic.Hash(plaintext) id := restic.Hash(plaintext)
var prefix string var prefix string
if !id.Equal(blob.ID) { if !id.Equal(blob.ID) {
Printf(" successfully %vdecrypted blob (length %v), hash is %v, ID does not match, wanted %v\n", outputPrefix, len(plaintext), id, 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-" prefix = "wrong-hash-"
} else { } else {
Printf(" successfully %vdecrypted blob (length %v), hash is %v, ID matches\n", outputPrefix, len(plaintext), id) printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID matches", outputPrefix, len(plaintext), id)
prefix = "correct-" prefix = "correct-"
} }
if opts.ExtractPack { if opts.ExtractPack {
err = storePlainBlob(id, filePrefix+prefix, plaintext) err = storePlainBlob(id, filePrefix+prefix, plaintext, printer)
if err != nil { if err != nil {
return err return err
} }
} }
if opts.ReuploadBlobs { if opts.ReuploadBlobs {
_, _, _, err := repo.SaveBlob(ctx, blob.Type, plaintext, id, true) _, _, _, err := uploader.SaveBlob(ctx, blob.Type, plaintext, id, true)
if err != nil { if err != nil {
return err return err
} }
Printf(" uploaded %v %v\n", blob.Type, id) printer.S(" uploaded %v %v", blob.Type, id)
} }
} }
if opts.ReuploadBlobs {
return repo.Flush(ctx)
}
return nil return nil
}) })
return err
return wg.Wait()
} }
func storePlainBlob(id restic.ID, prefix string, plain []byte) error { func storePlainBlob(id restic.ID, prefix string, plain []byte, printer progress.Printer) error {
filename := fmt.Sprintf("%s%s.bin", prefix, id) filename := fmt.Sprintf("%s%s.bin", prefix, id)
f, err := os.Create(filename) f, err := os.Create(filename)
if err != nil { if err != nil {
@@ -446,16 +439,18 @@ func storePlainBlob(id restic.ID, prefix string, plain []byte) error {
return err return err
} }
Printf("decrypt of blob %v stored at %v\n", id, filename) printer.S("decrypt of blob %v stored at %v", id, filename)
return nil return nil
} }
func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamineOptions, args []string) error { func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamineOptions, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
if opts.ExtractPack && gopts.NoLock { if opts.ExtractPack && gopts.NoLock {
return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive") return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive")
} }
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, gopts.NoLock) ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
@@ -467,7 +462,7 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
if err != nil { if err != nil {
id, err = restic.Find(ctx, repo, restic.PackFile, name) id, err = restic.Find(ctx, repo, restic.PackFile, name)
if err != nil { if err != nil {
Warnf("error: %v\n", err) printer.E("error: %v", err)
continue continue
} }
} }
@@ -478,16 +473,15 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
return errors.Fatal("no pack files to examine") return errors.Fatal("no pack files to examine")
} }
bar := newIndexProgress(gopts.Quiet, gopts.JSON) err = repo.LoadIndex(ctx, printer)
err = repo.LoadIndex(ctx, bar)
if err != nil { if err != nil {
return err return err
} }
for _, id := range ids { for _, id := range ids {
err := examinePack(ctx, opts, repo, id) err := examinePack(ctx, opts, repo, id, printer)
if err != nil { if err != nil {
Warnf("error: %v\n", err) printer.E("error: %v", err)
} }
if err == context.Canceled { if err == context.Canceled {
break break
@@ -496,24 +490,24 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
return nil return nil
} }
func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID) error { func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID, printer progress.Printer) error {
Printf("examine %v\n", id) printer.S("examine %v", id)
buf, err := repo.LoadRaw(ctx, restic.PackFile, id) buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
// also process damaged pack files // also process damaged pack files
if buf == nil { if buf == nil {
return err return err
} }
Printf(" file size is %v\n", len(buf)) printer.S(" file size is %v", len(buf))
gotID := restic.Hash(buf) gotID := restic.Hash(buf)
if !id.Equal(gotID) { if !id.Equal(gotID) {
Printf(" wanted hash %v, got %v\n", id, gotID) printer.S(" wanted hash %v, got %v", id, gotID)
} else { } else {
Printf(" hash for file content matches\n") printer.S(" hash for file content matches")
} }
Printf(" ========================================\n") printer.S(" ========================================")
Printf(" looking for info in the indexes\n") printer.S(" looking for info in the indexes")
blobsLoaded := false blobsLoaded := false
// examine all data the indexes have for the pack file // examine all data the indexes have for the pack file
@@ -523,43 +517,41 @@ func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repo
continue continue
} }
checkPackSize(blobs, len(buf)) checkPackSize(blobs, len(buf), printer)
err = loadBlobs(ctx, opts, repo, id, blobs) err = loadBlobs(ctx, opts, repo, id, blobs, printer)
if err != nil { if err != nil {
Warnf("error: %v\n", err) printer.E("error: %v", err)
} else { } else {
blobsLoaded = true blobsLoaded = true
} }
} }
Printf(" ========================================\n") printer.S(" ========================================")
Printf(" inspect the pack itself\n") printer.S(" inspect the pack itself")
blobs, _, err := repo.ListPack(ctx, id, int64(len(buf))) blobs, _, err := repo.ListPack(ctx, id, int64(len(buf)))
if err != nil { if err != nil {
return fmt.Errorf("pack %v: %v", id.Str(), err) return fmt.Errorf("pack %v: %v", id.Str(), err)
} }
checkPackSize(blobs, len(buf)) checkPackSize(blobs, len(buf), printer)
if !blobsLoaded { if !blobsLoaded {
return loadBlobs(ctx, opts, repo, id, blobs) return loadBlobs(ctx, opts, repo, id, blobs, printer)
} }
return nil return nil
} }
func checkPackSize(blobs []restic.Blob, fileSize int) { func checkPackSize(blobs restic.Blobs, fileSize int, printer progress.Printer) {
// track current size and offset // track current size and offset
var size, offset uint64 var size, offset uint64
sort.Slice(blobs, func(i, j int) bool { blobs.Sort()
return blobs[i].Offset < blobs[j].Offset
})
for _, pb := range blobs { for _, pb := range blobs {
Printf(" %v blob %v, offset %-6d, raw length %-6d\n", pb.Type, pb.ID, pb.Offset, pb.Length) printer.S(" %v blob %v, offset %-6d, raw length %-6d", pb.Type, pb.ID, pb.Offset, pb.Length)
if offset != uint64(pb.Offset) { if offset != uint64(pb.Offset) {
Printf(" hole in file, want offset %v, got %v\n", offset, pb.Offset) printer.S(" hole in file, want offset %v, got %v", offset, pb.Offset)
} }
offset = uint64(pb.Offset + pb.Length) offset = uint64(pb.Offset + pb.Length)
size += uint64(pb.Length) size += uint64(pb.Length)
@@ -567,8 +559,8 @@ func checkPackSize(blobs []restic.Blob, fileSize int) {
size += uint64(pack.CalculateHeaderSize(blobs)) size += uint64(pack.CalculateHeaderSize(blobs))
if uint64(fileSize) != size { if uint64(fileSize) != size {
Printf(" file sizes do not match: computed %v, file size is %v\n", size, fileSize) printer.S(" file sizes do not match: computed %v, file size is %v", size, fileSize)
} else { } else {
Printf(" file sizes match\n") printer.S(" file sizes match")
} }
} }
+5 -2
View File
@@ -2,8 +2,11 @@
package main package main
import "github.com/spf13/cobra" import (
"github.com/restic/restic/internal/global"
"github.com/spf13/cobra"
)
func registerDebugCommand(_ *cobra.Command) { func registerDebugCommand(_ *cobra.Command, _ *global.Options) {
// No commands to register in non-debug mode // No commands to register in non-debug mode
} }
+102 -106
View File
@@ -5,17 +5,18 @@ import (
"encoding/json" "encoding/json"
"path" "path"
"reflect" "reflect"
"sort"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newDiffCommand() *cobra.Command { func newDiffCommand(globalOptions *global.Options) *cobra.Command {
var opts DiffOptions var opts DiffOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -38,7 +39,7 @@ Metadata comparison will likely not work if a backup was created using the
To only compare files in specific subfolders, you can use the To only compare files in specific subfolders, you can use the
"snapshotID:subfolder" syntax, where "subfolder" is a path within the "snapshotID:subfolder" syntax, where "subfolder" is a path within the
snapshot. snapshot tree as shown by "restic ls".
EXIT STATUS EXIT STATUS
=========== ===========
@@ -52,7 +53,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDiff(cmd.Context(), opts, globalOptions, args) return runDiff(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
}, },
} }
@@ -69,10 +70,10 @@ func (opts *DiffOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.ShowMetadata, "metadata", false, "print changes in metadata") f.BoolVar(&opts.ShowMetadata, "metadata", false, "print changes in metadata")
} }
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*restic.Snapshot, string, error) { func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*data.Snapshot, string, error) {
sn, subfolder, err := restic.FindSnapshot(ctx, be, repo, desc) sn, subfolder, err := data.FindSnapshot(ctx, be, repo, desc)
if err != nil { if err != nil {
return nil, "", errors.Fatal(err.Error()) return nil, "", errors.Fatalf("%s", err)
} }
return sn, subfolder, err return sn, subfolder, err
} }
@@ -82,6 +83,7 @@ type Comparer struct {
repo restic.BlobLoader repo restic.BlobLoader
opts DiffOptions opts DiffOptions
printChange func(change *Change) printChange func(change *Change)
printError func(string, ...interface{})
} }
type Change struct { type Change struct {
@@ -105,15 +107,15 @@ type DiffStat struct {
} }
// Add adds stats information for node to s. // Add adds stats information for node to s.
func (s *DiffStat) Add(node *restic.Node) { func (s *DiffStat) Add(node *data.Node) {
if node == nil { if node == nil {
return return
} }
switch node.Type { switch node.Type {
case restic.NodeTypeFile: case data.NodeTypeFile:
s.Files++ s.Files++
case restic.NodeTypeDir: case data.NodeTypeDir:
s.Dirs++ s.Dirs++
default: default:
s.Others++ s.Others++
@@ -121,13 +123,13 @@ func (s *DiffStat) Add(node *restic.Node) {
} }
// addBlobs adds the blobs of node to s. // addBlobs adds the blobs of node to s.
func addBlobs(bs restic.BlobSet, node *restic.Node) { func addBlobs(bs restic.AssociatedBlobSet, node *data.Node) {
if node == nil { if node == nil {
return return
} }
switch node.Type { switch node.Type {
case restic.NodeTypeFile: case data.NodeTypeFile:
for _, blob := range node.Content { for _, blob := range node.Content {
h := restic.BlobHandle{ h := restic.BlobHandle{
ID: blob, ID: blob,
@@ -135,7 +137,7 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
} }
bs.Insert(h) bs.Insert(h)
} }
case restic.NodeTypeDir: case data.NodeTypeDir:
h := restic.BlobHandle{ h := restic.BlobHandle{
ID: *node.Subtree, ID: *node.Subtree,
Type: restic.TreeBlob, Type: restic.TreeBlob,
@@ -145,18 +147,18 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
} }
type DiffStatsContainer struct { type DiffStatsContainer struct {
MessageType string `json:"message_type"` // "statistics" MessageType string `json:"message_type"` // "statistics"
SourceSnapshot string `json:"source_snapshot"` SourceSnapshot string `json:"source_snapshot"`
TargetSnapshot string `json:"target_snapshot"` TargetSnapshot string `json:"target_snapshot"`
ChangedFiles int `json:"changed_files"` ChangedFiles int `json:"changed_files"`
Added DiffStat `json:"added"` Added DiffStat `json:"added"`
Removed DiffStat `json:"removed"` Removed DiffStat `json:"removed"`
BlobsBefore, BlobsAfter, BlobsCommon restic.BlobSet `json:"-"` BlobsBefore, BlobsAfter, BlobsCommon restic.AssociatedBlobSet `json:"-"`
} }
// updateBlobs updates the blob counters in the stats struct. // updateBlobs updates the blob counters in the stats struct.
func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) { func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *DiffStat, printError func(string, ...interface{})) {
for h := range blobs { for h := range blobs.Keys() {
switch h.Type { switch h.Type {
case restic.DataBlob: case restic.DataBlob:
stats.DataBlobs++ stats.DataBlobs++
@@ -166,7 +168,7 @@ func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) {
size, found := repo.LookupBlobSize(h.Type, h.ID) size, found := repo.LookupBlobSize(h.Type, h.ID)
if !found { if !found {
Warnf("unable to find blob size for %v\n", h) printError("unable to find blob size for %v", h)
continue continue
} }
@@ -174,30 +176,33 @@ func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) {
} }
} }
func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, blobs restic.BlobSet, prefix string, id restic.ID) error { func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, blobs restic.AssociatedBlobSet, prefix string, id restic.ID) error {
debug.Log("print %v tree %v", mode, id) debug.Log("print %v tree %v", mode, id)
tree, err := restic.LoadTree(ctx, c.repo, id) tree, err := data.LoadTree(ctx, c.repo, id)
if err != nil { if err != nil {
return err return err
} }
for _, node := range tree.Nodes { for item := range tree {
if item.Error != nil {
return item.Error
}
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
node := item.Node
name := path.Join(prefix, node.Name) name := path.Join(prefix, node.Name)
if node.Type == restic.NodeTypeDir { if node.Type == data.NodeTypeDir {
name += "/" name += "/"
} }
c.printChange(NewChange(name, mode)) c.printChange(NewChange(name, mode))
stats.Add(node) stats.Add(node)
addBlobs(blobs, node) addBlobs(blobs, node)
if node.Type == restic.NodeTypeDir { if node.Type == data.NodeTypeDir {
err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree) err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree)
if err != nil && err != context.Canceled { if err != nil && err != context.Canceled {
Warnf("error: %v\n", err) c.printError("error: %v", err)
} }
} }
} }
@@ -205,24 +210,28 @@ func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, b
return ctx.Err() return ctx.Err()
} }
func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id restic.ID) error { func (c *Comparer) collectDir(ctx context.Context, blobs restic.AssociatedBlobSet, id restic.ID) error {
debug.Log("print tree %v", id) debug.Log("print tree %v", id)
tree, err := restic.LoadTree(ctx, c.repo, id) tree, err := data.LoadTree(ctx, c.repo, id)
if err != nil { if err != nil {
return err return err
} }
for _, node := range tree.Nodes { for item := range tree {
if item.Error != nil {
return item.Error
}
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
node := item.Node
addBlobs(blobs, node) addBlobs(blobs, node)
if node.Type == restic.NodeTypeDir { if node.Type == data.NodeTypeDir {
err := c.collectDir(ctx, blobs, *node.Subtree) err := c.collectDir(ctx, blobs, *node.Subtree)
if err != nil && err != context.Canceled { if err != nil && err != context.Canceled {
Warnf("error: %v\n", err) c.printError("error: %v", err)
} }
} }
} }
@@ -230,56 +239,41 @@ func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id rest
return ctx.Err() return ctx.Err()
} }
func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[string]*restic.Node, uniqueNames []string) {
names := make(map[string]struct{})
tree1Nodes = make(map[string]*restic.Node)
for _, node := range tree1.Nodes {
tree1Nodes[node.Name] = node
names[node.Name] = struct{}{}
}
tree2Nodes = make(map[string]*restic.Node)
for _, node := range tree2.Nodes {
tree2Nodes[node.Name] = node
names[node.Name] = struct{}{}
}
uniqueNames = make([]string, 0, len(names))
for name := range names {
uniqueNames = append(uniqueNames, name)
}
sort.Strings(uniqueNames)
return tree1Nodes, tree2Nodes, uniqueNames
}
func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, prefix string, id1, id2 restic.ID) error { func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, prefix string, id1, id2 restic.ID) error {
debug.Log("diffing %v to %v", id1, id2) debug.Log("diffing %v to %v", id1, id2)
tree1, err := restic.LoadTree(ctx, c.repo, id1) tree1, err := data.LoadTree(ctx, c.repo, id1)
if err != nil { if err != nil {
return err return err
} }
tree2, err := restic.LoadTree(ctx, c.repo, id2) tree2, err := data.LoadTree(ctx, c.repo, id2)
if err != nil { if err != nil {
return err return err
} }
tree1Nodes, tree2Nodes, names := uniqueNodeNames(tree1, tree2) for dt := range data.DualTreeIterator(tree1, tree2) {
if dt.Error != nil {
for _, name := range names { return dt.Error
}
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
node1, t1 := tree1Nodes[name] node1 := dt.Tree1
node2, t2 := tree2Nodes[name] node2 := dt.Tree2
var name string
if node1 != nil {
name = node1.Name
} else {
name = node2.Name
}
addBlobs(stats.BlobsBefore, node1) addBlobs(stats.BlobsBefore, node1)
addBlobs(stats.BlobsAfter, node2) addBlobs(stats.BlobsAfter, node2)
switch { switch {
case t1 && t2: case node1 != nil && node2 != nil:
name := path.Join(prefix, name) name := path.Join(prefix, name)
mod := "" mod := ""
@@ -287,12 +281,12 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
mod += "T" mod += "T"
} }
if node2.Type == restic.NodeTypeDir { if node2.Type == data.NodeTypeDir {
name += "/" name += "/"
} }
if node1.Type == restic.NodeTypeFile && if node1.Type == data.NodeTypeFile &&
node2.Type == restic.NodeTypeFile && node2.Type == data.NodeTypeFile &&
!reflect.DeepEqual(node1.Content, node2.Content) { !reflect.DeepEqual(node1.Content, node2.Content) {
mod += "M" mod += "M"
stats.ChangedFiles++ stats.ChangedFiles++
@@ -314,7 +308,7 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
c.printChange(NewChange(name, mod)) c.printChange(NewChange(name, mod))
} }
if node1.Type == restic.NodeTypeDir && node2.Type == restic.NodeTypeDir { if node1.Type == data.NodeTypeDir && node2.Type == data.NodeTypeDir {
var err error var err error
if (*node1.Subtree).Equal(*node2.Subtree) { if (*node1.Subtree).Equal(*node2.Subtree) {
err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree) err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree)
@@ -322,35 +316,35 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree) err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
} }
if err != nil && err != context.Canceled { if err != nil && err != context.Canceled {
Warnf("error: %v\n", err) c.printError("error: %v", err)
} }
} }
case t1 && !t2: case node1 != nil && node2 == nil:
prefix := path.Join(prefix, name) prefix := path.Join(prefix, name)
if node1.Type == restic.NodeTypeDir { if node1.Type == data.NodeTypeDir {
prefix += "/" prefix += "/"
} }
c.printChange(NewChange(prefix, "-")) c.printChange(NewChange(prefix, "-"))
stats.Removed.Add(node1) stats.Removed.Add(node1)
if node1.Type == restic.NodeTypeDir { if node1.Type == data.NodeTypeDir {
err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree) err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree)
if err != nil && err != context.Canceled { if err != nil && err != context.Canceled {
Warnf("error: %v\n", err) c.printError("error: %v", err)
} }
} }
case !t1 && t2: case node1 == nil && node2 != nil:
prefix := path.Join(prefix, name) prefix := path.Join(prefix, name)
if node2.Type == restic.NodeTypeDir { if node2.Type == data.NodeTypeDir {
prefix += "/" prefix += "/"
} }
c.printChange(NewChange(prefix, "+")) c.printChange(NewChange(prefix, "+"))
stats.Added.Add(node2) stats.Added.Add(node2)
if node2.Type == restic.NodeTypeDir { if node2.Type == data.NodeTypeDir {
err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree) err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree)
if err != nil && err != context.Canceled { if err != nil && err != context.Canceled {
Warnf("error: %v\n", err) c.printError("error: %v", err)
} }
} }
} }
@@ -359,12 +353,14 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
return ctx.Err() return ctx.Err()
} }
func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []string) error { func runDiff(ctx context.Context, opts DiffOptions, gopts global.Options, args []string, term ui.Terminal) error {
if len(args) != 2 { if len(args) != 2 {
return errors.Fatalf("specify two snapshot IDs") return errors.Fatalf("specify two snapshot IDs")
} }
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
@@ -386,10 +382,9 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
} }
if !gopts.JSON { if !gopts.JSON {
Verbosef("comparing snapshot %v to %v:\n\n", sn1.ID().Str(), sn2.ID().Str()) printer.P("comparing snapshot %v to %v:\n\n", sn1.ID().Str(), sn2.ID().Str())
} }
bar := newIndexProgress(gopts.Quiet, gopts.JSON) if err = repo.LoadIndex(ctx, printer); err != nil {
if err = repo.LoadIndex(ctx, bar); err != nil {
return err return err
} }
@@ -401,30 +396,31 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
return errors.Errorf("snapshot %v has nil tree", sn2.ID().Str()) return errors.Errorf("snapshot %v has nil tree", sn2.ID().Str())
} }
sn1.Tree, err = restic.FindTreeDirectory(ctx, repo, sn1.Tree, subfolder1) sn1.Tree, err = data.FindTreeDirectory(ctx, repo, sn1.Tree, subfolder1)
if err != nil { if err != nil {
return err return err
} }
sn2.Tree, err = restic.FindTreeDirectory(ctx, repo, sn2.Tree, subfolder2) sn2.Tree, err = data.FindTreeDirectory(ctx, repo, sn2.Tree, subfolder2)
if err != nil { if err != nil {
return err return err
} }
c := &Comparer{ c := &Comparer{
repo: repo, repo: repo,
opts: opts, opts: opts,
printError: printer.E,
printChange: func(change *Change) { printChange: func(change *Change) {
Printf("%-5s%v\n", change.Modifier, change.Path) printer.S("%-5s%v", change.Modifier, change.Path)
}, },
} }
if gopts.JSON { if gopts.JSON {
enc := json.NewEncoder(globalOptions.stdout) enc := json.NewEncoder(gopts.Term.OutputWriter())
c.printChange = func(change *Change) { c.printChange = func(change *Change) {
err := enc.Encode(change) err := enc.Encode(change)
if err != nil { if err != nil {
Warnf("JSON encode failed: %v\n", err) printer.E("JSON encode failed: %v", err)
} }
} }
} }
@@ -437,9 +433,9 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
MessageType: "statistics", MessageType: "statistics",
SourceSnapshot: args[0], SourceSnapshot: args[0],
TargetSnapshot: args[1], TargetSnapshot: args[1],
BlobsBefore: restic.NewBlobSet(), BlobsBefore: repo.NewAssociatedBlobSet(),
BlobsAfter: restic.NewBlobSet(), BlobsAfter: repo.NewAssociatedBlobSet(),
BlobsCommon: restic.NewBlobSet(), BlobsCommon: repo.NewAssociatedBlobSet(),
} }
stats.BlobsBefore.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn1.Tree}) stats.BlobsBefore.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn1.Tree})
stats.BlobsAfter.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn2.Tree}) stats.BlobsAfter.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn2.Tree})
@@ -450,23 +446,23 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
} }
both := stats.BlobsBefore.Intersect(stats.BlobsAfter) both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed) updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed, printer.E)
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added) updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added, printer.E)
if gopts.JSON { if gopts.JSON {
err := json.NewEncoder(globalOptions.stdout).Encode(stats) err := json.NewEncoder(gopts.Term.OutputWriter()).Encode(stats)
if err != nil { if err != nil {
Warnf("JSON encode failed: %v\n", err) printer.E("JSON encode failed: %v", err)
} }
} else { } else {
Printf("\n") printer.S("")
Printf("Files: %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles) printer.S("Files: %5d new, %5d removed, %5d changed", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
Printf("Dirs: %5d new, %5d removed\n", stats.Added.Dirs, stats.Removed.Dirs) printer.S("Dirs: %5d new, %5d removed", stats.Added.Dirs, stats.Removed.Dirs)
Printf("Others: %5d new, %5d removed\n", stats.Added.Others, stats.Removed.Others) printer.S("Others: %5d new, %5d removed", stats.Added.Others, stats.Removed.Others)
Printf("Data Blobs: %5d new, %5d removed\n", stats.Added.DataBlobs, stats.Removed.DataBlobs) printer.S("Data Blobs: %5d new, %5d removed", stats.Added.DataBlobs, stats.Removed.DataBlobs)
Printf("Tree Blobs: %5d new, %5d removed\n", stats.Added.TreeBlobs, stats.Removed.TreeBlobs) printer.S("Tree Blobs: %5d new, %5d removed", stats.Added.TreeBlobs, stats.Removed.TreeBlobs)
Printf(" Added: %-5s\n", ui.FormatBytes(stats.Added.Bytes)) printer.S(" Added: %-5s", ui.FormatBytes(stats.Added.Bytes))
Printf(" Removed: %-5s\n", ui.FormatBytes(stats.Removed.Bytes)) printer.S(" Removed: %-5s", ui.FormatBytes(stats.Removed.Bytes))
} }
return nil return nil
+9 -8
View File
@@ -11,15 +11,16 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/restic/restic/internal/global"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func testRunDiffOutput(gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) { func testRunDiffOutput(t testing.TB, gopts global.Options, firstSnapshotID string, secondSnapshotID string) (string, error) {
buf, err := withCaptureStdout(func() error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
opts := DiffOptions{ opts := DiffOptions{
ShowMetadata: false, ShowMetadata: false,
} }
return runDiff(context.TODO(), opts, gopts, []string{firstSnapshotID, secondSnapshotID}) return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.Term)
}) })
return buf.String(), err return buf.String(), err
} }
@@ -123,10 +124,10 @@ func TestDiff(t *testing.T) {
// quiet suppresses the diff output except for the summary // quiet suppresses the diff output except for the summary
env.gopts.Quiet = false env.gopts.Quiet = false
_, err := testRunDiffOutput(env.gopts, "", secondSnapshotID) _, err := testRunDiffOutput(t, env.gopts, "", secondSnapshotID)
rtest.Assert(t, err != nil, "expected error on invalid snapshot id") rtest.Assert(t, err != nil, "expected error on invalid snapshot id")
out, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID) out, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
rtest.OK(t, err) rtest.OK(t, err)
for _, pattern := range diffOutputRegexPatterns { for _, pattern := range diffOutputRegexPatterns {
@@ -137,7 +138,7 @@ func TestDiff(t *testing.T) {
// check quiet output // check quiet output
env.gopts.Quiet = true env.gopts.Quiet = true
outQuiet, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID) outQuiet, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
rtest.OK(t, err) rtest.OK(t, err)
rtest.Assert(t, len(outQuiet) < len(out), "expected shorter output on quiet mode %v vs. %v", len(outQuiet), len(out)) rtest.Assert(t, len(outQuiet) < len(out), "expected shorter output on quiet mode %v vs. %v", len(outQuiet), len(out))
@@ -154,7 +155,7 @@ func TestDiffJSON(t *testing.T) {
// quiet suppresses the diff output except for the summary // quiet suppresses the diff output except for the summary
env.gopts.Quiet = false env.gopts.Quiet = false
env.gopts.JSON = true env.gopts.JSON = true
out, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID) out, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
rtest.OK(t, err) rtest.OK(t, err)
var stat DiffStatsContainer var stat DiffStatsContainer
@@ -181,7 +182,7 @@ func TestDiffJSON(t *testing.T) {
// check quiet output // check quiet output
env.gopts.Quiet = true env.gopts.Quiet = true
outQuiet, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID) outQuiet, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
rtest.OK(t, err) rtest.OK(t, err)
stat = DiffStatsContainer{} stat = DiffStatsContainer{}
+35 -31
View File
@@ -7,20 +7,23 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/dump" "github.com/restic/restic/internal/dump"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newDumpCommand() *cobra.Command { func newDumpCommand(globalOptions *global.Options) *cobra.Command {
var opts DumpOptions var opts DumpOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dump [flags] snapshotID file", Use: "dump [flags] snapshotID file",
Short: "Print a backed-up file to stdout", Short: "Print backed-up files or folders to stdout",
Long: ` Long: `
The "dump" command extracts files from a snapshot from the repository. If a The "dump" command extracts files from a snapshot from the repository. If a
single file is selected, it prints its contents to stdout. Folders are output single file is selected, it prints its contents to stdout. Folders are output
@@ -32,7 +35,7 @@ repository.
To include the folder content at the root of the archive, you can use the To include the folder content at the root of the archive, you can use the
"snapshotID:subfolder" syntax, where "subfolder" is a path within the "snapshotID:subfolder" syntax, where "subfolder" is a path within the
snapshot. snapshot tree as shown by "restic ls".
EXIT STATUS EXIT STATUS
=========== ===========
@@ -46,7 +49,8 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDump(cmd.Context(), opts, globalOptions, args) finalizeSnapshotFilter(&opts.SnapshotFilter)
return runDump(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
}, },
} }
@@ -56,7 +60,7 @@ Exit status is 12 if the password is incorrect.
// DumpOptions collects all options for the dump command. // DumpOptions collects all options for the dump command.
type DumpOptions struct { type DumpOptions struct {
restic.SnapshotFilter data.SnapshotFilter
Archive string Archive string
Target string Target string
} }
@@ -76,7 +80,7 @@ func splitPath(p string) []string {
return append(s, f) return append(s, f)
} }
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoader, prefix string, pathComponents []string, d *dump.Dumper, canWriteArchiveFunc func() error) error { func printFromTree(ctx context.Context, tree data.TreeNodeIterator, repo restic.BlobLoader, prefix string, pathComponents []string, d *dump.Dumper, canWriteArchiveFunc func() error) error {
// If we print / we need to assume that there are multiple nodes at that // If we print / we need to assume that there are multiple nodes at that
// level in the tree. // level in the tree.
if pathComponents[0] == "" { if pathComponents[0] == "" {
@@ -88,35 +92,38 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
item := filepath.Join(prefix, pathComponents[0]) item := filepath.Join(prefix, pathComponents[0])
l := len(pathComponents) l := len(pathComponents)
for _, node := range tree.Nodes { for it := range tree {
if it.Error != nil {
return it.Error
}
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
node := it.Node
// If dumping something in the highest level it will just take the // If dumping something in the highest level it will just take the
// first item it finds and dump that according to the switch case below. // first item it finds and dump that according to the switch case below.
if node.Name == pathComponents[0] { if node.Name == pathComponents[0] {
switch { switch {
case l == 1 && node.Type == restic.NodeTypeFile: case l == 1 && node.Type == data.NodeTypeFile:
return d.WriteNode(ctx, node) return d.WriteNode(ctx, node)
case l > 1 && node.Type == restic.NodeTypeDir: case l > 1 && node.Type == data.NodeTypeDir:
subtree, err := restic.LoadTree(ctx, repo, *node.Subtree) subtree, err := data.LoadTree(ctx, repo, *node.Subtree)
if err != nil { if err != nil {
return errors.Wrapf(err, "cannot load subtree for %q", item) return errors.Wrapf(err, "cannot load subtree for %q", item)
} }
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], d, canWriteArchiveFunc) return printFromTree(ctx, subtree, repo, item, pathComponents[1:], d, canWriteArchiveFunc)
case node.Type == restic.NodeTypeDir: case node.Type == data.NodeTypeDir:
if err := canWriteArchiveFunc(); err != nil { if err := canWriteArchiveFunc(); err != nil {
return err return err
} }
subtree, err := restic.LoadTree(ctx, repo, *node.Subtree) subtree, err := data.LoadTree(ctx, repo, *node.Subtree)
if err != nil { if err != nil {
return err return err
} }
return d.DumpTree(ctx, subtree, item) return d.DumpTree(ctx, subtree, item)
case l > 1: case l > 1:
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type) return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
case node.Type != restic.NodeTypeFile: case node.Type != data.NodeTypeFile:
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type) return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
} }
} }
@@ -124,11 +131,13 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
return fmt.Errorf("path %q not found in snapshot", item) return fmt.Errorf("path %q not found in snapshot", item)
} }
func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []string) error { func runDump(ctx context.Context, opts DumpOptions, gopts global.Options, args []string, term ui.Terminal) error {
if len(args) != 2 { if len(args) != 2 {
return errors.Fatal("no file and no snapshot ID specified") return errors.Fatal("no file and no snapshot ID specified")
} }
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
switch opts.Archive { switch opts.Archive {
case "tar", "zip": case "tar", "zip":
default: default:
@@ -142,39 +151,34 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
splittedPath := splitPath(path.Clean(pathToPrint)) splittedPath := splitPath(path.Clean(pathToPrint))
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
defer unlock() defer unlock()
sn, subfolder, err := (&restic.SnapshotFilter{ sn, subfolder, err := opts.SnapshotFilter.FindLatest(ctx, repo, repo, snapshotIDString)
Hosts: opts.Hosts,
Paths: opts.Paths,
Tags: opts.Tags,
}).FindLatest(ctx, repo, repo, snapshotIDString)
if err != nil { if err != nil {
return errors.Fatalf("failed to find snapshot: %v", err) return errors.Fatalf("failed to find snapshot: %v", err)
} }
bar := newIndexProgress(gopts.Quiet, gopts.JSON) err = repo.LoadIndex(ctx, printer)
err = repo.LoadIndex(ctx, bar)
if err != nil { if err != nil {
return err return err
} }
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subfolder) sn.Tree, err = data.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
if err != nil { if err != nil {
return err return err
} }
tree, err := restic.LoadTree(ctx, repo, *sn.Tree) tree, err := data.LoadTree(ctx, repo, *sn.Tree)
if err != nil { if err != nil {
return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err) return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err)
} }
outputFileWriter := os.Stdout outputFileWriter := term.OutputRaw()
canWriteArchiveFunc := checkStdoutArchive canWriteArchiveFunc := checkStdoutArchive(term)
if opts.Target != "" { if opts.Target != "" {
file, err := os.Create(opts.Target) file, err := os.Create(opts.Target)
@@ -198,9 +202,9 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
return nil return nil
} }
func checkStdoutArchive() error { func checkStdoutArchive(term ui.Terminal) func() error {
if stdoutIsTerminal() { if term.OutputIsTerminal() {
return fmt.Errorf("stdout is the terminal, please redirect output") return func() error { return fmt.Errorf("stdout is the terminal, please redirect output") }
} }
return nil return func() error { return nil }
} }
+4 -5
View File
@@ -1,16 +1,15 @@
package main package main
import ( import (
"fmt"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/feature" "github.com/restic/restic/internal/feature"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/ui/table" "github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func newFeaturesCommand() *cobra.Command { func newFeaturesCommand(globalOptions *global.Options) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "features", Use: "features",
Short: "Print list of feature flags", Short: "Print list of feature flags",
@@ -39,7 +38,7 @@ Exit status is 1 if there was any error.
return errors.Fatal("the feature command expects no arguments") return errors.Fatal("the feature command expects no arguments")
} }
fmt.Printf("All Feature Flags:\n") globalOptions.Term.Print("All Feature Flags:\n")
flags := feature.Flag.List() flags := feature.Flag.List()
tab := table.New() tab := table.New()
@@ -51,7 +50,7 @@ Exit status is 1 if there was any error.
for _, flag := range flags { for _, flag := range flags {
tab.AddRow(flag) tab.AddRow(flag)
} }
return tab.Write(globalOptions.stdout) return tab.Write(globalOptions.Term.OutputWriter())
}, },
} }
+106 -66
View File
@@ -3,6 +3,8 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -10,14 +12,20 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/filter"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
) )
func newFindCommand() *cobra.Command { // errFindDone is returned from the tree walk when all requested tree IDs were found.
var errFindDone = errors.New("find: all tree IDs found")
func newFindCommand(globalOptions *global.Options) *cobra.Command {
var opts FindOptions var opts FindOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -25,16 +33,11 @@ func newFindCommand() *cobra.Command {
Short: "Find a file, a directory or restic IDs", Short: "Find a file, a directory or restic IDs",
Long: ` Long: `
The "find" command searches for files or directories in snapshots stored in the The "find" command searches for files or directories in snapshots stored in the
repo. repository. It can also be used to search for restic blobs, trees or pack
It can also be used to search for restic blobs or trees for troubleshooting. files for troubleshooting.
The default sort option for the snapshots is youngest to oldest. To sort the The default sort option for the snapshots is youngest to oldest. To sort the
output from oldest to youngest specify --reverse.`, output from oldest to youngest specify --reverse.
Example: `restic find config.json
restic find --json "*.yml" "*.json"
restic find --json --blob 420f620f b46ebe8a ddd38656
restic find --show-pack-id --blob 420f620f
restic find --tree 577c2bc9 f81f2e22 a62827a9
restic find --pack 025c1d06
EXIT STATUS EXIT STATUS
=========== ===========
@@ -45,10 +48,17 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. Exit status is 11 if the repository is already locked.
Exit status is 12 if the password is incorrect. Exit status is 12 if the password is incorrect.
`, `,
Example: `restic find config.json
restic find --json "*.yml" "*.json"
restic find --json --blob 420f620f b46ebe8a ddd38656
restic find --show-pack-id --blob 420f620f
restic find --tree 577c2bc9 f81f2e22 a62827a9
restic find --pack 025c1d06`,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runFind(cmd.Context(), opts, globalOptions, args) finalizeSnapshotFilter(&opts.SnapshotFilter)
return runFind(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
}, },
} }
@@ -67,7 +77,7 @@ type FindOptions struct {
ListLong bool ListLong bool
HumanReadable bool HumanReadable bool
Reverse bool Reverse bool
restic.SnapshotFilter data.SnapshotFilter
} }
func (opts *FindOptions) AddFlags(f *pflag.FlagSet) { func (opts *FindOptions) AddFlags(f *pflag.FlagSet) {
@@ -121,13 +131,19 @@ type statefulOutput struct {
HumanReadable bool HumanReadable bool
JSON bool JSON bool
inuse bool inuse bool
newsn *restic.Snapshot newsn *data.Snapshot
oldsn *restic.Snapshot oldsn *data.Snapshot
hits int hits int
printer interface {
S(string, ...interface{})
P(string, ...interface{})
E(string, ...interface{})
}
stdout io.Writer
} }
func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) { func (s *statefulOutput) PrintPatternJSON(path string, node *data.Node) {
type findNode restic.Node type findNode data.Node
b, err := json.Marshal(struct { b, err := json.Marshal(struct {
// Add these attributes // Add these attributes
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
@@ -148,40 +164,40 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
findNode: (*findNode)(node), findNode: (*findNode)(node),
}) })
if err != nil { if err != nil {
Warnf("Marshall failed: %v\n", err) s.printer.E("Marshal failed: %v", err)
return return
} }
if !s.inuse { if !s.inuse {
Printf("[") _, _ = s.stdout.Write([]byte("["))
s.inuse = true s.inuse = true
} }
if s.newsn != s.oldsn { if s.newsn != s.oldsn {
if s.oldsn != nil { if s.oldsn != nil {
Printf("],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID()) _, _ = fmt.Fprintf(s.stdout, "],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID())
} }
Printf(`{"matches":[`) _, _ = s.stdout.Write([]byte(`{"matches":[`))
s.oldsn = s.newsn s.oldsn = s.newsn
s.hits = 0 s.hits = 0
} }
if s.hits > 0 { if s.hits > 0 {
Printf(",") _, _ = s.stdout.Write([]byte(","))
} }
Print(string(b)) _, _ = s.stdout.Write(b)
s.hits++ s.hits++
} }
func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) { func (s *statefulOutput) PrintPatternNormal(path string, node *data.Node) {
if s.newsn != s.oldsn { if s.newsn != s.oldsn {
if s.oldsn != nil { if s.oldsn != nil {
Verbosef("\n") s.printer.P("")
} }
s.oldsn = s.newsn s.oldsn = s.newsn
Verbosef("Found matching entries in snapshot %s from %s\n", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(TimeFormat)) s.printer.P("Found matching entries in snapshot %s from %s", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(global.TimeFormat))
} }
Println(formatNode(path, node, s.ListLong, s.HumanReadable)) s.printer.S(formatNode(path, node, s.ListLong, s.HumanReadable))
} }
func (s *statefulOutput) PrintPattern(path string, node *restic.Node) { func (s *statefulOutput) PrintPattern(path string, node *data.Node) {
if s.JSON { if s.JSON {
s.PrintPatternJSON(path, node) s.PrintPatternJSON(path, node)
} else { } else {
@@ -189,7 +205,7 @@ func (s *statefulOutput) PrintPattern(path string, node *restic.Node) {
} }
} }
func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *restic.Snapshot) { func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *data.Snapshot) {
b, err := json.Marshal(struct { b, err := json.Marshal(struct {
// Add these attributes // Add these attributes
ObjectType string `json:"object_type"` ObjectType string `json:"object_type"`
@@ -207,32 +223,32 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *
Time: sn.Time, Time: sn.Time,
}) })
if err != nil { if err != nil {
Warnf("Marshall failed: %v\n", err) s.printer.E("Marshal failed: %v", err)
return return
} }
if !s.inuse { if !s.inuse {
Printf("[") _, _ = s.stdout.Write([]byte("["))
s.inuse = true s.inuse = true
} }
if s.hits > 0 { if s.hits > 0 {
Printf(",") _, _ = s.stdout.Write([]byte(","))
} }
Print(string(b)) _, _ = s.stdout.Write(b)
s.hits++ s.hits++
} }
func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *restic.Snapshot) { func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *data.Snapshot) {
Printf("Found %s %s\n", kind, id) s.printer.S("Found %s %s", kind, id)
if kind == "blob" { if kind == "blob" {
Printf(" ... in file %s\n", nodepath) s.printer.S(" ... in file %s", nodepath)
Printf(" (tree %s)\n", treeID) s.printer.S(" (tree %s)", treeID)
} else { } else {
Printf(" ... path %s\n", nodepath) s.printer.S(" ... path %s", nodepath)
} }
Printf(" ... in snapshot %s (%s)\n", sn.ID().Str(), sn.Time.Local().Format(TimeFormat)) s.printer.S(" ... in snapshot %s (%s)", sn.ID().Str(), sn.Time.Local().Format(global.TimeFormat))
} }
func (s *statefulOutput) PrintObject(kind, id, nodepath, treeID string, sn *restic.Snapshot) { func (s *statefulOutput) PrintObject(kind, id, nodepath, treeID string, sn *data.Snapshot) {
if s.JSON { if s.JSON {
s.PrintObjectJSON(kind, id, nodepath, treeID, sn) s.PrintObjectJSON(kind, id, nodepath, treeID, sn)
} else { } else {
@@ -244,12 +260,12 @@ func (s *statefulOutput) Finish() {
if s.JSON { if s.JSON {
// do some finishing up // do some finishing up
if s.oldsn != nil { if s.oldsn != nil {
Printf("],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID()) _, _ = fmt.Fprintf(s.stdout, "],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID())
} }
if s.inuse { if s.inuse {
Printf("]\n") _, _ = s.stdout.Write([]byte("]\n"))
} else { } else {
Printf("[]\n") _, _ = s.stdout.Write([]byte("[]\n"))
} }
return return
} }
@@ -263,9 +279,14 @@ type Finder struct {
blobIDs map[string]struct{} blobIDs map[string]struct{}
treeIDs map[string]struct{} treeIDs map[string]struct{}
itemsFound int itemsFound int
printer interface {
S(string, ...interface{})
P(string, ...interface{})
E(string, ...interface{})
}
} }
func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error { func (f *Finder) findInSnapshot(ctx context.Context, sn *data.Snapshot) error {
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), f.pat.oldest, f.pat.newest) debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), f.pat.oldest, f.pat.newest)
if sn.Tree == nil { if sn.Tree == nil {
@@ -273,11 +294,12 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
} }
f.out.newsn = sn f.out.newsn = sn
return walker.Walk(ctx, f.repo, *sn.Tree, walker.WalkVisitor{ProcessNode: func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) error { return walker.Walk(ctx, f.repo, *sn.Tree, walker.WalkVisitor{ProcessNode: func(parentTreeID restic.ID, nodepath string, node *data.Node, err error) error {
if err != nil { if err != nil {
debug.Log("Error loading tree %v: %v", parentTreeID, err) debug.Log("Error loading tree %v: %v", parentTreeID, err)
Printf("Unable to load tree %s\n ... which belongs to snapshot %s\n", parentTreeID, sn.ID()) f.printer.S("Unable to load tree %s", parentTreeID)
f.printer.S(" ... which belongs to snapshot %s", sn.ID())
return walker.ErrSkipNode return walker.ErrSkipNode
} }
@@ -305,7 +327,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
} }
var errIfNoMatch error var errIfNoMatch error
if node.Type == restic.NodeTypeDir { if node.Type == data.NodeTypeDir {
var childMayMatch bool var childMayMatch bool
for _, pat := range f.pat.pattern { for _, pat := range f.pat.pattern {
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath) mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
@@ -357,13 +379,13 @@ func (f *Finder) findTree(treeID restic.ID, nodepath string) error {
// looking for blobs) // looking for blobs)
if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil { if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil {
// Return an error to terminate the Walk // Return an error to terminate the Walk
return errors.New("OK") return errFindDone
} }
} }
return nil return nil
} }
func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error { func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
debug.Log("searching IDs in snapshot %s", sn.ID()) debug.Log("searching IDs in snapshot %s", sn.ID())
if sn.Tree == nil { if sn.Tree == nil {
@@ -371,11 +393,12 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
} }
f.out.newsn = sn f.out.newsn = sn
return walker.Walk(ctx, f.repo, *sn.Tree, walker.WalkVisitor{ProcessNode: func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) error { return walker.Walk(ctx, f.repo, *sn.Tree, walker.WalkVisitor{ProcessNode: func(parentTreeID restic.ID, nodepath string, node *data.Node, err error) error {
if err != nil { if err != nil {
debug.Log("Error loading tree %v: %v", parentTreeID, err) debug.Log("Error loading tree %v: %v", parentTreeID, err)
Printf("Unable to load tree %s\n ... which belongs to snapshot %s\n", parentTreeID, sn.ID()) f.printer.S("Unable to load tree %s", parentTreeID)
f.printer.S(" ... which belongs to snapshot %s", sn.ID())
return walker.ErrSkipNode return walker.ErrSkipNode
} }
@@ -395,7 +418,7 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
} }
} }
if node.Type == restic.NodeTypeFile && f.blobIDs != nil { if node.Type == data.NodeTypeFile && f.blobIDs != nil {
for _, id := range node.Content { for _, id := range node.Content {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
@@ -431,6 +454,9 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
if f.blobIDs == nil { if f.blobIDs == nil {
f.blobIDs = make(map[string]struct{}) f.blobIDs = make(map[string]struct{})
} }
if f.treeIDs == nil {
f.treeIDs = make(map[string]struct{})
}
debug.Log("Looking for packs...") debug.Log("Looking for packs...")
err := f.repo.List(ctx, restic.PackFile, func(id restic.ID, size int64) error { err := f.repo.List(ctx, restic.PackFile, func(id restic.ID, size int64) error {
@@ -451,7 +477,14 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
return err return err
} }
for _, b := range blobs { for _, b := range blobs {
f.blobIDs[b.ID.String()] = struct{}{} 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()))
}
} }
// Stop searching when all packs have been found // Stop searching when all packs have been found
if len(packIDs) == 0 { if len(packIDs) == 0 {
@@ -524,7 +557,7 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
for h := range indexPackIDs { for h := range indexPackIDs {
list = append(list, h) list = append(list, h)
} }
Warnf("some pack files are missing from the repository, getting their blobs from the repository index: %v\n\n", list) 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 return packIDs, nil
} }
@@ -532,19 +565,20 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
func (f *Finder) findObjectPack(id string, t restic.BlobType) { func (f *Finder) findObjectPack(id string, t restic.BlobType) {
rid, err := restic.ParseID(id) rid, err := restic.ParseID(id)
if err != nil { if err != nil {
Printf("Note: cannot find pack for object '%s', unable to parse ID: %v\n", id, err) f.printer.S("Note: cannot find pack for object '%s', unable to parse ID: %v", id, err)
return return
} }
blobs := f.repo.LookupBlob(t, rid) blobs := f.repo.LookupBlob(t, rid)
if len(blobs) == 0 { if len(blobs) == 0 {
Printf("Object %s not found in the index\n", rid.Str()) f.printer.S("Object %s with type %s not found in the index", t.String(), rid.Str())
return return
} }
for _, b := range blobs { for _, b := range blobs {
if b.ID.Equal(rid) { if b.ID.Equal(rid) {
Printf("Object belongs to pack %s\n ... Pack %s: %s\n", b.PackID, b.PackID.Str(), b.String()) f.printer.S("Object belongs to pack %s", b.PackID)
f.printer.S(" ... Pack %s: %s", b.PackID.Str(), b.String())
break break
} }
} }
@@ -560,11 +594,13 @@ func (f *Finder) findObjectsPacks() {
} }
} }
func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []string) error { func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args []string, term ui.Terminal) error {
if len(args) == 0 { if len(args) == 0 {
return errors.Fatal("wrong number of arguments") return errors.Fatal("wrong number of arguments")
} }
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
var err error var err error
pat := findPattern{pattern: args} pat := findPattern{pattern: args}
if opts.CaseInsensitive { if opts.CaseInsensitive {
@@ -586,6 +622,10 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
} }
} }
if !pat.newest.IsZero() && !pat.oldest.IsZero() && pat.oldest.After(pat.newest) {
return errors.Fatal("--oldest must specify a time before --newest")
}
// Check at most only one kind of IDs is provided: currently we // Check at most only one kind of IDs is provided: currently we
// can't mix types // can't mix types
if (opts.BlobID && opts.TreeID) || if (opts.BlobID && opts.TreeID) ||
@@ -594,7 +634,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
return errors.Fatal("cannot have several ID types") return errors.Fatal("cannot have several ID types")
} }
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
@@ -604,15 +644,15 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
if err != nil { if err != nil {
return err return err
} }
bar := newIndexProgress(gopts.Quiet, gopts.JSON) if err = repo.LoadIndex(ctx, printer); err != nil {
if err = repo.LoadIndex(ctx, bar); err != nil {
return err return err
} }
f := &Finder{ f := &Finder{
repo: repo, repo: repo,
pat: pat, pat: pat,
out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON}, out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON, printer: printer, stdout: term.OutputRaw()},
printer: printer,
} }
if opts.BlobID { if opts.BlobID {
@@ -635,8 +675,8 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
} }
} }
var filteredSnapshots []*restic.Snapshot var filteredSnapshots []*data.Snapshot
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots) { for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots, printer) {
filteredSnapshots = append(filteredSnapshots, sn) filteredSnapshots = append(filteredSnapshots, sn)
} }
if ctx.Err() != nil { if ctx.Err() != nil {
@@ -652,7 +692,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
for _, sn := range filteredSnapshots { for _, sn := range filteredSnapshots {
if f.blobIDs != nil || f.treeIDs != nil { if f.blobIDs != nil || f.treeIDs != nil {
if err = f.findIDs(ctx, sn); err != nil && err.Error() != "OK" { if err = f.findIDs(ctx, sn); err != nil && !errors.Is(err, errFindDone) {
return err return err
} }
continue continue
+149 -6
View File
@@ -3,18 +3,23 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
) )
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts GlobalOptions, pattern string) []byte { func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts global.Options, pattern string) []byte {
buf, err := withCaptureStdout(func() error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
gopts.JSON = wantJSON gopts.JSON = wantJSON
return runFind(context.TODO(), opts, gopts, []string{pattern}) return runFind(ctx, opts, gopts, []string{pattern}, gopts.Term)
}) })
rtest.OK(t, err) rtest.OK(t, err)
return buf.Bytes() return buf.Bytes()
@@ -95,7 +100,7 @@ func TestFindSorting(t *testing.T) {
env, cleanup := withTestEnvironment(t) env, cleanup := withTestEnvironment(t)
defer cleanup() defer cleanup()
datafile := testSetupBackupData(t, env) testSetupBackupData(t, env)
opts := BackupOptions{} opts := BackupOptions{}
// first backup // first backup
@@ -114,14 +119,14 @@ func TestFindSorting(t *testing.T) {
// first restic find - with default FindOptions{} // first restic find - with default FindOptions{}
results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile") results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
lines := strings.Split(string(results), "\n") lines := strings.Split(string(results), "\n")
rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines)) rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines))
matches := []testMatches{} matches := []testMatches{}
rtest.OK(t, json.Unmarshal(results, &matches)) rtest.OK(t, json.Unmarshal(results, &matches))
// run second restic find with --reverse, sort oldest to newest // run second restic find with --reverse, sort oldest to newest
resultsReverse := testRunFind(t, true, FindOptions{Reverse: true}, env.gopts, "testfile") resultsReverse := testRunFind(t, true, FindOptions{Reverse: true}, env.gopts, "testfile")
lines = strings.Split(string(resultsReverse), "\n") lines = strings.Split(string(resultsReverse), "\n")
rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines)) rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines))
matchesReverse := []testMatches{} matchesReverse := []testMatches{}
rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse)) rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse))
@@ -131,3 +136,141 @@ func TestFindSorting(t *testing.T) {
rtest.Assert(t, matches[0].SnapshotID == matchesReverse[1].SnapshotID, "matches should be sorted 1") rtest.Assert(t, matches[0].SnapshotID == matchesReverse[1].SnapshotID, "matches should be sorted 1")
rtest.Assert(t, matches[1].SnapshotID == matchesReverse[0].SnapshotID, "matches should be sorted 2") rtest.Assert(t, matches[1].SnapshotID == matchesReverse[0].SnapshotID, "matches should be sorted 2")
} }
func TestFindInvalidTimeRange(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
err := runFind(context.TODO(), FindOptions{Oldest: "2026-01-01", Newest: "2020-01-01"}, env.gopts, []string{"quack"}, env.gopts.Term)
rtest.Assert(t, err != nil && err.Error() == "Fatal: --oldest must specify a time before --newest",
"unexpected error message: %v", err)
}
// JsonOutput is the struct `restic find --json` produces
type JSONOutput struct {
ObjectType string `json:"object_type"`
ID string `json:"id"`
Path string `json:"path"`
ParentTree string `json:"parent_tree,omitempty"`
SnapshotID string `json:"snapshot"`
Time time.Time `json:"time,omitempty"`
}
func TestFindPackfile(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
// backup
backupPath := env.testdata + "/0/0/9"
testRunBackup(t, "", []string{backupPath}, BackupOptions{}, env.gopts)
sn1 := testListSnapshots(t, env.gopts, 1)[0]
// 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)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
// load master index
rtest.OK(t, repo.LoadIndex(ctx, printer))
packID := restic.ID{}
done := false
err = repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
if !done && pb.Type == restic.TreeBlob {
packID = pb.PackID
done = true
}
})
rtest.OK(t, err)
rtest.Assert(t, !packID.IsNull(), "expected a tree packfile ID")
findOptions := FindOptions{PackID: true}
results := testRunFind(t, true, findOptions, env.gopts, packID.String())
// get the json records
jsonResult := []JSONOutput{}
rtest.OK(t, json.Unmarshal(results, &jsonResult))
rtest.Assert(t, len(jsonResult) > 0, "expected at least one tree record in the packfile")
// look at the last record
lastIndex := len(jsonResult) - 1
record := jsonResult[lastIndex]
rtest.Assert(t, record.ObjectType == "tree" && record.SnapshotID == sn1.String(),
"expected a tree record with known snapshot id, but got type=%s and snapID=%s instead of %s",
record.ObjectType, record.SnapshotID, sn1.String())
backupPath = filepath.ToSlash(backupPath)[2:] // take the offending drive mapping away
rtest.Assert(t, strings.Contains(record.Path, backupPath), "expected %q as part of %q", backupPath, record.Path)
return nil
})
rtest.OK(t, err)
}
func TestFindPackID(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
dir009 := filepath.Join(env.testdata, "0", "0", "9")
dirEntries, err := os.ReadDir(dir009)
rtest.OK(t, err)
numberOfFiles := len(dirEntries)
// backup
testRunBackup(t, "", []string{dir009}, BackupOptions{}, env.gopts)
sn1 := testListSnapshots(t, env.gopts, 1)[0]
// extract packfile ID from repository index
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)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
// load Index
rtest.OK(t, repo.LoadIndex(ctx, nil))
// 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 {
case restic.DataBlob:
dataPackID = blob.PackID
case restic.TreeBlob:
treePackID = blob.PackID
}
}))
return nil
})
rtest.OK(t, err)
// look for data packfile
rtest.Assert(t, !dataPackID.IsNull(), "expected to find data packfile in repo")
packID := dataPackID.String()
out := testRunFind(t, true, FindOptions{PackID: true}, env.gopts, packID)
findRes := []JSONOutput{}
rtest.OK(t, json.Unmarshal(out, &findRes))
rtest.Assert(t, len(findRes) == numberOfFiles, "expected %d entries for this 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()
out = testRunFind(t, true, FindOptions{PackID: true}, env.gopts, packID)
findRes = []JSONOutput{}
rtest.OK(t, json.Unmarshal(out, &findRes))
record := findRes[len(findRes)-1]
rtest.Equals(t, record.ObjectType, "tree")
rtest.Equals(t, record.SnapshotID, sn1.String())
// windows path are messy, so we get rid of the messy bits at the start
// exp: "/C/Users/RUNNER~1/AppData/Local/Temp/restic-test-2921201257/testdata/0/0/9"
// got: "C:/Users/RUNNER~1/AppData/Local/Temp/restic-test-2921201257/testdata/0/0/9"
rtest.Equals(t, filepath.ToSlash(record.Path)[2:], filepath.ToSlash(dir009)[2:])
}
+47 -38
View File
@@ -7,14 +7,16 @@ import (
"io" "io"
"strconv" "strconv"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newForgetCommand() *cobra.Command { func newForgetCommand(globalOptions *global.Options) *cobra.Command {
var opts ForgetOptions var opts ForgetOptions
var pruneOpts PruneOptions var pruneOpts PruneOptions
@@ -41,6 +43,7 @@ EXIT STATUS
Exit status is 0 if the command was successful. Exit status is 0 if the command was successful.
Exit status is 1 if there was any error. Exit status is 1 if there was any error.
Exit status is 3 if there was an error removing one or more snapshots.
Exit status is 10 if the repository does not exist. Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. Exit status is 11 if the repository is already locked.
Exit status is 12 if the password is incorrect. Exit status is 12 if the password is incorrect.
@@ -48,9 +51,8 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() finalizeSnapshotFilter(&opts.SnapshotFilter)
defer cancel() return runForget(cmd.Context(), opts, pruneOpts, *globalOptions, globalOptions.Term, args)
return runForget(cmd.Context(), opts, pruneOpts, globalOptions, term, args)
}, },
} }
@@ -62,6 +64,7 @@ Exit status is 12 if the password is incorrect.
type ForgetPolicyCount int type ForgetPolicyCount int
var ErrNegativePolicyCount = errors.New("negative values not allowed, use 'unlimited' instead") var ErrNegativePolicyCount = errors.New("negative values not allowed, use 'unlimited' instead")
var ErrFailedToRemoveOneOrMoreSnapshots = errors.New("failed to remove one or more snapshots")
func (c *ForgetPolicyCount) Set(s string) error { func (c *ForgetPolicyCount) Set(s string) error {
switch s { switch s {
@@ -102,21 +105,21 @@ type ForgetOptions struct {
Weekly ForgetPolicyCount Weekly ForgetPolicyCount
Monthly ForgetPolicyCount Monthly ForgetPolicyCount
Yearly ForgetPolicyCount Yearly ForgetPolicyCount
Within restic.Duration Within data.Duration
WithinHourly restic.Duration WithinHourly data.Duration
WithinDaily restic.Duration WithinDaily data.Duration
WithinWeekly restic.Duration WithinWeekly data.Duration
WithinMonthly restic.Duration WithinMonthly data.Duration
WithinYearly restic.Duration WithinYearly data.Duration
KeepTags restic.TagLists KeepTags data.TagLists
UnsafeAllowRemoveAll bool UnsafeAllowRemoveAll bool
restic.SnapshotFilter data.SnapshotFilter
Compact bool Compact bool
// Grouping // Grouping
GroupBy restic.SnapshotGroupByOptions GroupBy data.SnapshotGroupByOptions
DryRun bool DryRun bool
Prune bool Prune bool
} }
@@ -147,7 +150,7 @@ func (opts *ForgetOptions) AddFlags(f *pflag.FlagSet) {
initMultiSnapshotFilter(f, &opts.SnapshotFilter, false) initMultiSnapshotFilter(f, &opts.SnapshotFilter, false)
f.BoolVarP(&opts.Compact, "compact", "c", false, "use compact output format") f.BoolVarP(&opts.Compact, "compact", "c", false, "use compact output format")
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true} opts.GroupBy = data.SnapshotGroupByOptions{Host: true, Path: true}
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')") f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done") f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
f.BoolVar(&opts.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed") f.BoolVar(&opts.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
@@ -161,7 +164,7 @@ func verifyForgetOptions(opts *ForgetOptions) error {
return errors.Fatal("negative values other than -1 are not allowed for --keep-*") return errors.Fatal("negative values other than -1 are not allowed for --keep-*")
} }
for _, d := range []restic.Duration{opts.Within, opts.WithinHourly, opts.WithinDaily, for _, d := range []data.Duration{opts.Within, opts.WithinHourly, opts.WithinDaily,
opts.WithinMonthly, opts.WithinWeekly, opts.WithinYearly} { opts.WithinMonthly, opts.WithinWeekly, opts.WithinYearly} {
if d.Hours < 0 || d.Days < 0 || d.Months < 0 || d.Years < 0 { if d.Hours < 0 || d.Days < 0 || d.Months < 0 || d.Years < 0 {
return errors.Fatal("durations containing negative values are not allowed for --keep-within*") return errors.Fatal("durations containing negative values are not allowed for --keep-within*")
@@ -171,7 +174,7 @@ func verifyForgetOptions(opts *ForgetOptions) error {
return nil return nil
} }
func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error { func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOptions, gopts global.Options, term ui.Terminal, args []string) error {
err := verifyForgetOptions(&opts) err := verifyForgetOptions(&opts)
if err != nil { if err != nil {
return err return err
@@ -186,22 +189,17 @@ 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") return errors.Fatal("--no-lock is only applicable in combination with --dry-run for forget command")
} }
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock) printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
defer unlock() defer unlock()
verbosity := gopts.verbosity var snapshots data.Snapshots
if gopts.JSON {
verbosity = 0
}
printer := newTerminalProgressPrinter(verbosity, term)
var snapshots restic.Snapshots
removeSnIDs := restic.NewIDSet() removeSnIDs := restic.NewIDSet()
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) { for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
snapshots = append(snapshots, sn) snapshots = append(snapshots, sn)
} }
if ctx.Err() != nil { if ctx.Err() != nil {
@@ -216,12 +214,12 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
removeSnIDs.Insert(*sn.ID()) removeSnIDs.Insert(*sn.ID())
} }
} else { } else {
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy) snapshotGroups, _, err := data.GroupSnapshots(snapshots, opts.GroupBy)
if err != nil { if err != nil {
return err return err
} }
policy := restic.ExpirePolicy{ policy := data.ExpirePolicy{
Last: int(opts.Last), Last: int(opts.Last),
Hourly: int(opts.Hourly), Hourly: int(opts.Hourly),
Daily: int(opts.Daily), Daily: int(opts.Daily),
@@ -256,14 +254,14 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
} }
if gopts.Verbose >= 1 && !gopts.JSON { if gopts.Verbose >= 1 && !gopts.JSON {
err = PrintSnapshotGroupHeader(globalOptions.stdout, k) err = PrintSnapshotGroupHeader(gopts.Term.OutputWriter(), k)
if err != nil { if err != nil {
return err return err
} }
} }
var key restic.SnapshotGroupKey var key data.SnapshotGroupKey
if json.Unmarshal([]byte(k), &key) != nil { if err := json.Unmarshal([]byte(k), &key); err != nil {
return err return err
} }
@@ -272,21 +270,25 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
fg.Host = key.Hostname fg.Host = key.Hostname
fg.Paths = key.Paths fg.Paths = key.Paths
keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy) keep, remove, reasons := data.ApplyPolicy(snapshotGroup, policy)
if !policy.Empty() && len(keep) == 0 { if !policy.Empty() && len(keep) == 0 {
return fmt.Errorf("refusing to delete last snapshot of snapshot group \"%v\"", key.String()) return fmt.Errorf("refusing to delete last snapshot of snapshot group \"%v\"", key.String())
} }
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON { if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
printer.P("keep %d snapshots:\n", len(keep)) printer.P("keep %d snapshots:\n", len(keep))
PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact) if err := PrintSnapshots(gopts.Term.OutputWriter(), keep, reasons, opts.Compact); err != nil {
return err
}
printer.P("\n") printer.P("\n")
} }
fg.Keep = asJSONSnapshots(keep) fg.Keep = asJSONSnapshots(keep)
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON { if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
printer.P("remove %d snapshots:\n", len(remove)) printer.P("remove %d snapshots:\n", len(remove))
PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact) if err := PrintSnapshots(gopts.Term.OutputWriter(), remove, nil, opts.Compact); err != nil {
return err
}
printer.P("\n") printer.P("\n")
} }
fg.Remove = asJSONSnapshots(remove) fg.Remove = asJSONSnapshots(remove)
@@ -305,12 +307,15 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
return ctx.Err() return ctx.Err()
} }
// these are the snapshots that failed to be removed
failedSnIDs := restic.NewIDSet()
if len(removeSnIDs) > 0 { if len(removeSnIDs) > 0 {
if !opts.DryRun { if !opts.DryRun {
bar := printer.NewCounter("files deleted") bar := printer.NewCounter("files deleted")
err := restic.ParallelRemove(ctx, repo, removeSnIDs, restic.WriteableSnapshotFile, func(id restic.ID, err error) error { err := restic.ParallelRemove(ctx, repo, removeSnIDs, restic.WriteableSnapshotFile, func(id restic.ID, err error) error {
if err != nil { if err != nil {
printer.E("unable to remove %v/%v from the repository\n", restic.SnapshotFile, id) printer.E("unable to remove %v/%v from the repository\n", restic.SnapshotFile, id)
failedSnIDs.Insert(id)
} else { } else {
printer.VV("removed %v/%v\n", restic.SnapshotFile, id) printer.VV("removed %v/%v\n", restic.SnapshotFile, id)
} }
@@ -326,12 +331,16 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
} }
if gopts.JSON && len(jsonGroups) > 0 { if gopts.JSON && len(jsonGroups) > 0 {
err = printJSONForget(globalOptions.stdout, jsonGroups) err = printJSONForget(gopts.Term.OutputWriter(), jsonGroups)
if err != nil { if err != nil {
return err return err
} }
} }
if len(failedSnIDs) > 0 {
return ErrFailedToRemoveOneOrMoreSnapshots
}
if len(removeSnIDs) > 0 && opts.Prune { if len(removeSnIDs) > 0 && opts.Prune {
if opts.DryRun { if opts.DryRun {
printer.P("%d snapshots would be removed, running prune dry run\n", len(removeSnIDs)) printer.P("%d snapshots would be removed, running prune dry run\n", len(removeSnIDs))
@@ -339,7 +348,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs)) printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs))
} }
pruneOptions.DryRun = opts.DryRun pruneOptions.DryRun = opts.DryRun
return runPruneWithRepo(ctx, pruneOptions, gopts, repo, removeSnIDs, term) return runPruneWithRepo(ctx, pruneOptions, repo, removeSnIDs, printer)
} }
return nil return nil
@@ -355,7 +364,7 @@ type ForgetGroup struct {
Reasons []KeepReason `json:"reasons"` Reasons []KeepReason `json:"reasons"`
} }
func asJSONSnapshots(list restic.Snapshots) []Snapshot { func asJSONSnapshots(list data.Snapshots) []Snapshot {
var resultList []Snapshot var resultList []Snapshot
for _, sn := range list { for _, sn := range list {
k := Snapshot{ k := Snapshot{
@@ -374,7 +383,7 @@ type KeepReason struct {
Matches []string `json:"matches"` Matches []string `json:"matches"`
} }
func asJSONKeeps(list []restic.KeepReason) []KeepReason { func asJSONKeeps(list []data.KeepReason) []KeepReason {
var resultList []KeepReason var resultList []KeepReason
for _, keep := range list { for _, keep := range list {
k := KeepReason{ k := KeepReason{
+14 -14
View File
@@ -6,22 +6,22 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/global"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/termstatus"
) )
func testRunForgetMayFail(gopts GlobalOptions, opts ForgetOptions, args ...string) error { func testRunForgetMayFail(t testing.TB, gopts global.Options, opts ForgetOptions, args ...string) error {
pruneOpts := PruneOptions{ pruneOpts := PruneOptions{
MaxUnused: "5%", MaxUnused: "5%",
} }
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { return withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runForget(context.TODO(), opts, pruneOpts, gopts, term, args) return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.Term, args)
}) })
} }
func testRunForget(t testing.TB, gopts GlobalOptions, opts ForgetOptions, args ...string) { func testRunForget(t testing.TB, gopts global.Options, opts ForgetOptions, args ...string) {
rtest.OK(t, testRunForgetMayFail(gopts, opts, args...)) rtest.OK(t, testRunForgetMayFail(t, gopts, opts, args...))
} }
func TestRunForgetSafetyNet(t *testing.T) { func TestRunForgetSafetyNet(t *testing.T) {
@@ -38,27 +38,27 @@ func TestRunForgetSafetyNet(t *testing.T) {
testListSnapshots(t, env.gopts, 2) testListSnapshots(t, env.gopts, 2)
// --keep-tags invalid // --keep-tags invalid
err := testRunForgetMayFail(env.gopts, ForgetOptions{ err := testRunForgetMayFail(t, env.gopts, ForgetOptions{
KeepTags: restic.TagLists{restic.TagList{"invalid"}}, KeepTags: data.TagLists{data.TagList{"invalid"}},
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true}, GroupBy: data.SnapshotGroupByOptions{Host: true, Path: true},
}) })
rtest.Assert(t, strings.Contains(err.Error(), `refusing to delete last snapshot of snapshot group "host example, path`), "wrong error message got %v", err) rtest.Assert(t, strings.Contains(err.Error(), `refusing to delete last snapshot of snapshot group "host example, path`), "wrong error message got %v", err)
// disallow `forget --unsafe-allow-remove-all` // disallow `forget --unsafe-allow-remove-all`
err = testRunForgetMayFail(env.gopts, ForgetOptions{ err = testRunForgetMayFail(t, env.gopts, ForgetOptions{
UnsafeAllowRemoveAll: true, UnsafeAllowRemoveAll: true,
}) })
rtest.Assert(t, strings.Contains(err.Error(), `--unsafe-allow-remove-all is not allowed unless a snapshot filter option is specified`), "wrong error message got %v", err) rtest.Assert(t, strings.Contains(err.Error(), `--unsafe-allow-remove-all is not allowed unless a snapshot filter option is specified`), "wrong error message got %v", err)
// disallow `forget` without options // disallow `forget` without options
err = testRunForgetMayFail(env.gopts, ForgetOptions{}) err = testRunForgetMayFail(t, env.gopts, ForgetOptions{})
rtest.Assert(t, strings.Contains(err.Error(), `no policy was specified, no snapshots will be removed`), "wrong error message got %v", err) rtest.Assert(t, strings.Contains(err.Error(), `no policy was specified, no snapshots will be removed`), "wrong error message got %v", err)
// `forget --host example --unsafe-allow-remove-all` should work // `forget --host example --unsafe-allow-remove-all` should work
testRunForget(t, env.gopts, ForgetOptions{ testRunForget(t, env.gopts, ForgetOptions{
UnsafeAllowRemoveAll: true, UnsafeAllowRemoveAll: true,
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true}, GroupBy: data.SnapshotGroupByOptions{Host: true, Path: true},
SnapshotFilter: restic.SnapshotFilter{ SnapshotFilter: data.SnapshotFilter{
Hosts: []string{opts.Host}, Hosts: []string{opts.Host},
}, },
}) })
+47 -16
View File
@@ -3,7 +3,7 @@ package main
import ( import (
"testing" "testing"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/data"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@@ -69,18 +69,18 @@ func TestForgetOptionValues(t *testing.T) {
{ForgetOptions{Weekly: -2}, negValErrorMsg}, {ForgetOptions{Weekly: -2}, negValErrorMsg},
{ForgetOptions{Monthly: -2}, negValErrorMsg}, {ForgetOptions{Monthly: -2}, negValErrorMsg},
{ForgetOptions{Yearly: -2}, negValErrorMsg}, {ForgetOptions{Yearly: -2}, negValErrorMsg},
{ForgetOptions{Within: restic.ParseDurationOrPanic("1y2m3d3h")}, ""}, {ForgetOptions{Within: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y2m3d3h")}, ""}, {ForgetOptions{WithinHourly: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m3d3h")}, ""}, {ForgetOptions{WithinDaily: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d3h")}, ""}, {ForgetOptions{WithinWeekly: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("2y4m6d8h")}, ""}, {ForgetOptions{WithinMonthly: data.ParseDurationOrPanic("2y4m6d8h")}, ""},
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y4m6d8h")}, ""}, {ForgetOptions{WithinYearly: data.ParseDurationOrPanic("2y4m6d8h")}, ""},
{ForgetOptions{Within: restic.ParseDurationOrPanic("-1y2m3d3h")}, negDurationValErrorMsg}, {ForgetOptions{Within: data.ParseDurationOrPanic("-1y2m3d3h")}, negDurationValErrorMsg},
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y-2m3d3h")}, negDurationValErrorMsg}, {ForgetOptions{WithinHourly: data.ParseDurationOrPanic("1y-2m3d3h")}, negDurationValErrorMsg},
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m-3d3h")}, negDurationValErrorMsg}, {ForgetOptions{WithinDaily: data.ParseDurationOrPanic("1y2m-3d3h")}, negDurationValErrorMsg},
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d-3h")}, negDurationValErrorMsg}, {ForgetOptions{WithinWeekly: data.ParseDurationOrPanic("1y2m3d-3h")}, negDurationValErrorMsg},
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("-2y4m6d8h")}, negDurationValErrorMsg}, {ForgetOptions{WithinMonthly: data.ParseDurationOrPanic("-2y4m6d8h")}, negDurationValErrorMsg},
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y-4m6d8h")}, negDurationValErrorMsg}, {ForgetOptions{WithinYearly: data.ParseDurationOrPanic("2y-4m6d8h")}, negDurationValErrorMsg},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
@@ -96,7 +96,38 @@ func TestForgetOptionValues(t *testing.T) {
func TestForgetHostnameDefaulting(t *testing.T) { func TestForgetHostnameDefaulting(t *testing.T) {
t.Setenv("RESTIC_HOST", "testhost") t.Setenv("RESTIC_HOST", "testhost")
opts := ForgetOptions{}
opts.AddFlags(pflag.NewFlagSet("test", pflag.ContinueOnError)) tests := []struct {
rtest.Equals(t, []string{"testhost"}, opts.Hosts) name string
args []string
want []string
}{
{
name: "env default when flag not set",
args: nil,
want: []string{"testhost"},
},
{
name: "flag overrides env",
args: []string{"--host", "flaghost"},
want: []string{"flaghost"},
},
{
name: "empty flag clears env",
args: []string{"--host", ""},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
set := pflag.NewFlagSet(tt.name, pflag.ContinueOnError)
opts := ForgetOptions{}
opts.AddFlags(set)
err := set.Parse(tt.args)
rtest.Assert(t, err == nil, "expected no error for input")
finalizeSnapshotFilter(&opts.SnapshotFilter)
rtest.Equals(t, tt.want, opts.Hosts)
})
}
} }
+19 -17
View File
@@ -6,12 +6,15 @@ import (
"time" "time"
"github.com/restic/restic/internal/errors" "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/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newGenerateCommand() *cobra.Command { func newGenerateCommand(globalOptions *global.Options) *cobra.Command {
var opts generateOptions var opts generateOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -19,7 +22,7 @@ func newGenerateCommand() *cobra.Command {
Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)", Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)",
Long: ` Long: `
The "generate" command writes automatically generated files (like the man pages The "generate" command writes automatically generated files (like the man pages
and the auto-completion files for bash, fish and zsh). and the auto-completion files for bash, fish, powershell and zsh).
EXIT STATUS EXIT STATUS
=========== ===========
@@ -29,7 +32,7 @@ Exit status is 1 if there was any error.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
return runGenerate(opts, args) return runGenerate(opts, *globalOptions, args, globalOptions.Term)
}, },
} }
opts.AddFlags(cmd.Flags()) opts.AddFlags(cmd.Flags())
@@ -52,7 +55,7 @@ func (opts *generateOptions) AddFlags(f *pflag.FlagSet) {
f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)") f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
} }
func writeManpages(root *cobra.Command, dir string) error { func writeManpages(root *cobra.Command, dir string, printer progress.Printer) error {
// use a fixed date for the man pages so that generating them is deterministic // use a fixed date for the man pages so that generating them is deterministic
date, err := time.Parse("Jan 2006", "Jan 2017") date, err := time.Parse("Jan 2006", "Jan 2017")
if err != nil { if err != nil {
@@ -66,14 +69,12 @@ func writeManpages(root *cobra.Command, dir string) error {
Date: &date, Date: &date,
} }
Verbosef("writing man pages to directory %v\n", dir) printer.P("writing man pages to directory %v", dir)
return doc.GenManTree(root, header, dir) return doc.GenManTree(root, header, dir)
} }
func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) { func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts global.Options) (err error) {
if stdoutIsTerminal() { printer.PT("writing %s completion file to %v", shell, filename)
Verbosef("writing %s completion file to %v\n", shell, filename)
}
var outWriter io.Writer var outWriter io.Writer
if filename != "-" { if filename != "-" {
var outFile *os.File var outFile *os.File
@@ -84,7 +85,7 @@ func writeCompletion(filename string, shell string, generate func(w io.Writer) e
defer func() { err = outFile.Close() }() defer func() { err = outFile.Close() }()
outWriter = outFile outWriter = outFile
} else { } else {
outWriter = globalOptions.stdout outWriter = gopts.Term.OutputWriter()
} }
err = generate(outWriter) err = generate(outWriter)
@@ -110,15 +111,16 @@ func checkStdoutForSingleShell(opts generateOptions) error {
return nil return nil
} }
func runGenerate(opts generateOptions, args []string) error { func runGenerate(opts generateOptions, gopts global.Options, args []string, term ui.Terminal) error {
if len(args) > 0 { if len(args) > 0 {
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags") return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
} }
cmdRoot := newRootCommand() printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
cmdRoot := newRootCommand(&global.Options{})
if opts.ManDir != "" { if opts.ManDir != "" {
err := writeManpages(cmdRoot, opts.ManDir) err := writeManpages(cmdRoot, opts.ManDir, printer)
if err != nil { if err != nil {
return err return err
} }
@@ -130,28 +132,28 @@ func runGenerate(opts generateOptions, args []string) error {
} }
if opts.BashCompletionFile != "" { if opts.BashCompletionFile != "" {
err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion) err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion, printer, gopts)
if err != nil { if err != nil {
return err return err
} }
} }
if opts.FishCompletionFile != "" { if opts.FishCompletionFile != "" {
err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }) err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }, printer, gopts)
if err != nil { if err != nil {
return err return err
} }
} }
if opts.ZSHCompletionFile != "" { if opts.ZSHCompletionFile != "" {
err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion) err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion, printer, gopts)
if err != nil { if err != nil {
return err return err
} }
} }
if opts.PowerShellCompletionFile != "" { if opts.PowerShellCompletionFile != "" {
err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion) err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion, printer, gopts)
if err != nil { if err != nil {
return err return err
} }
+12 -10
View File
@@ -1,13 +1,21 @@
package main package main
import ( import (
"bytes" "context"
"strings" "strings"
"testing" "testing"
"github.com/restic/restic/internal/global"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func testRunGenerate(t testing.TB, gopts global.Options, opts generateOptions) ([]byte, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runGenerate(opts, gopts, []string{}, gopts.Term)
})
return buf.Bytes(), err
}
func TestGenerateStdout(t *testing.T) { func TestGenerateStdout(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
@@ -21,20 +29,14 @@ func TestGenerateStdout(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
buf := bytes.NewBuffer(nil) output, err := testRunGenerate(t, global.Options{}, tc.opts)
globalOptions.stdout = buf
err := runGenerate(tc.opts, []string{})
rtest.OK(t, err) rtest.OK(t, err)
completionString := buf.String() rtest.Assert(t, strings.Contains(string(output), "# "+tc.name+" completion for restic"), "has no expected completion header")
rtest.Assert(t, strings.Contains(completionString, "# "+tc.name+" completion for restic"), "has no expected completion header")
}) })
} }
t.Run("Generate shell completions to stdout for two shells", func(t *testing.T) { t.Run("Generate shell completions to stdout for two shells", func(t *testing.T) {
buf := bytes.NewBuffer(nil) _, err := testRunGenerate(t, global.Options{}, generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"})
globalOptions.stdout = buf
opts := generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"}
err := runGenerate(opts, []string{})
rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails") rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails")
}) })
} }
+28 -53
View File
@@ -8,14 +8,16 @@ import (
"github.com/restic/chunker" "github.com/restic/chunker"
"github.com/restic/restic/internal/backend/location" "github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "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/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newInitCommand() *cobra.Command { func newInitCommand(globalOptions *global.Options) *cobra.Command {
var opts InitOptions var opts InitOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -33,7 +35,7 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runInit(cmd.Context(), opts, globalOptions, args) return runInit(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
}, },
} }
opts.AddFlags(cmd.Flags()) opts.AddFlags(cmd.Flags())
@@ -42,105 +44,78 @@ Exit status is 1 if there was any error.
// InitOptions bundles all options for the init command. // InitOptions bundles all options for the init command.
type InitOptions struct { type InitOptions struct {
secondaryRepoOptions global.SecondaryRepoOptions
CopyChunkerParameters bool CopyChunkerParameters bool
RepositoryVersion string RepositoryVersion string
} }
func (opts *InitOptions) AddFlags(f *pflag.FlagSet) { func (opts *InitOptions) AddFlags(f *pflag.FlagSet) {
opts.secondaryRepoOptions.AddFlags(f, "secondary", "to copy chunker parameters from") opts.SecondaryRepoOptions.AddFlags(f, "secondary", "to copy chunker parameters from")
f.BoolVar(&opts.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)") f.BoolVar(&opts.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
f.StringVar(&opts.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'") f.StringVar(&opts.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'")
} }
func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []string) error { func runInit(ctx context.Context, opts InitOptions, gopts global.Options, args []string, term ui.Terminal) error {
if len(args) > 0 { if len(args) > 0 {
return errors.Fatal("the init command expects no arguments, only options - please see `restic help init` for usage and flags") 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)
var version uint var version uint
if opts.RepositoryVersion == "latest" || opts.RepositoryVersion == "" { switch opts.RepositoryVersion {
case "latest", "":
version = restic.MaxRepoVersion version = restic.MaxRepoVersion
} else if opts.RepositoryVersion == "stable" { case "stable":
version = restic.StableRepoVersion version = restic.StableRepoVersion
} else { default:
v, err := strconv.ParseUint(opts.RepositoryVersion, 10, 32) v, err := strconv.ParseUint(opts.RepositoryVersion, 10, 32)
if err != nil { if err != nil {
return errors.Fatal("invalid repository version") return errors.Fatal("invalid repository version")
} }
version = uint(v) version = uint(v)
} }
if version < restic.MinRepoVersion || version > restic.MaxRepoVersion {
return errors.Fatalf("only repository versions between %v and %v are allowed", restic.MinRepoVersion, restic.MaxRepoVersion)
}
chunkerPolynomial, err := maybeReadChunkerPolynomial(ctx, opts, gopts) chunkerPolynomial, err := maybeReadChunkerPolynomial(ctx, opts, gopts, printer)
if err != nil { if err != nil {
return err return err
} }
gopts.Repo, err = ReadRepo(gopts) s, err := global.CreateRepository(ctx, gopts, version, chunkerPolynomial, printer)
if err != nil { if err != nil {
return err return errors.Fatalf("%s", err)
}
gopts.password, err = ReadPasswordTwice(ctx, gopts,
"enter password for new repository: ",
"enter password again: ")
if err != nil {
return err
}
be, err := create(ctx, gopts.Repo, gopts, gopts.extended)
if err != nil {
return errors.Fatalf("create repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err)
}
s, err := repository.New(be, repository.Options{
Compression: gopts.Compression,
PackSize: gopts.PackSize * 1024 * 1024,
})
if err != nil {
return errors.Fatal(err.Error())
}
err = s.Init(ctx, version, gopts.password, chunkerPolynomial)
if err != nil {
return errors.Fatalf("create key in repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err)
} }
if !gopts.JSON { if !gopts.JSON {
Verbosef("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.backends, gopts.Repo)) printer.P("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.Backends, gopts.Repo))
if opts.CopyChunkerParameters && chunkerPolynomial != nil { if opts.CopyChunkerParameters && chunkerPolynomial != nil {
Verbosef(" with chunker parameters copied from secondary repository\n") printer.P(" with chunker parameters copied from secondary repository")
} else {
Verbosef("\n")
} }
Verbosef("\n") printer.P("")
Verbosef("Please note that knowledge of your password is required to access\n") printer.P("Please note that knowledge of your password is required to access")
Verbosef("the repository. Losing your password means that your data is\n") printer.P("the repository. Losing your password means that your data is")
Verbosef("irrecoverably lost.\n") printer.P("irrecoverably lost.")
} else { } else {
status := initSuccess{ status := initSuccess{
MessageType: "initialized", MessageType: "initialized",
ID: s.Config().ID, ID: s.Config().ID,
Repository: location.StripPassword(gopts.backends, gopts.Repo), Repository: location.StripPassword(gopts.Backends, gopts.Repo),
} }
return json.NewEncoder(globalOptions.stdout).Encode(status) return json.NewEncoder(gopts.Term.OutputWriter()).Encode(status)
} }
return nil return nil
} }
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts GlobalOptions) (*chunker.Pol, error) { func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer progress.Printer) (*chunker.Pol, error) {
if opts.CopyChunkerParameters { if opts.CopyChunkerParameters {
otherGopts, _, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "secondary") otherGopts, _, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "secondary")
if err != nil { if err != nil {
return nil, err return nil, err
} }
otherRepo, err := OpenRepository(ctx, otherGopts) otherRepo, err := global.OpenRepository(ctx, otherGopts, printer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+30 -11
View File
@@ -6,22 +6,27 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/progress"
) )
func testRunInit(t testing.TB, opts GlobalOptions) { func testRunInit(t testing.TB, gopts global.Options) {
repository.TestUseLowSecurityKDFParameters(t) repository.TestUseLowSecurityKDFParameters(t)
restic.TestDisableCheckPolynomial(t) restic.TestDisableCheckPolynomial(t)
restic.TestSetLockTimeout(t, 0) restic.TestSetLockTimeout(t, 0)
rtest.OK(t, runInit(context.TODO(), InitOptions{}, opts, nil)) err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
t.Logf("repository initialized at %v", opts.Repo) return runInit(ctx, InitOptions{}, gopts, nil, gopts.Term)
})
rtest.OK(t, err)
t.Logf("repository initialized at %v", gopts.Repo)
// create temporary junk files to verify that restic does not trip over them // 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")} { for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
rtest.OK(t, os.WriteFile(filepath.Join(opts.Repo, path, "tmp12345"), []byte("junk file"), 0o600)) rtest.OK(t, os.WriteFile(filepath.Join(gopts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
} }
} }
@@ -34,20 +39,34 @@ func TestInitCopyChunkerParams(t *testing.T) {
testRunInit(t, env2.gopts) testRunInit(t, env2.gopts)
initOpts := InitOptions{ initOpts := InitOptions{
secondaryRepoOptions: secondaryRepoOptions{ SecondaryRepoOptions: global.SecondaryRepoOptions{
Repo: env2.gopts.Repo, Repo: env2.gopts.Repo,
password: env2.gopts.password, Password: env2.gopts.Password,
}, },
} }
rtest.Assert(t, runInit(context.TODO(), initOpts, env.gopts, nil) != nil, "expected invalid init options to fail") err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runInit(ctx, initOpts, gopts, nil, gopts.Term)
})
rtest.Assert(t, err != nil, "expected invalid init options to fail")
initOpts.CopyChunkerParameters = true initOpts.CopyChunkerParameters = true
rtest.OK(t, runInit(context.TODO(), initOpts, env.gopts, nil)) err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runInit(ctx, initOpts, gopts, nil, gopts.Term)
repo, err := OpenRepository(context.TODO(), env.gopts) })
rtest.OK(t, err) rtest.OK(t, err)
otherRepo, err := OpenRepository(context.TODO(), env2.gopts) 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{})
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{})
return err
})
rtest.OK(t, err) rtest.OK(t, err)
rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial, rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial,
+6 -5
View File
@@ -1,10 +1,11 @@
package main package main
import ( import (
"github.com/restic/restic/internal/global"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func newKeyCommand() *cobra.Command { func newKeyCommand(globalOptions *global.Options) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "key", Use: "key",
Short: "Manage keys (passwords)", Short: "Manage keys (passwords)",
@@ -17,10 +18,10 @@ per repository.
} }
cmd.AddCommand( cmd.AddCommand(
newKeyAddCommand(), newKeyAddCommand(globalOptions),
newKeyListCommand(), newKeyListCommand(globalOptions),
newKeyPasswdCommand(), newKeyPasswdCommand(globalOptions),
newKeyRemoveCommand(), newKeyRemoveCommand(globalOptions),
) )
return cmd return cmd
} }
+17 -13
View File
@@ -5,19 +5,22 @@ import (
"fmt" "fmt"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "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/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newKeyAddCommand() *cobra.Command { func newKeyAddCommand(globalOptions *global.Options) *cobra.Command {
var opts KeyAddOptions var opts KeyAddOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "add", Use: "add",
Short: "Add a new key (password) to the repository; returns the new key ID", Short: "Add a new key (password) to the repository; returns the new key ID",
Long: ` Long: `
The "add" sub-command creates a new key and validates the key. Returns the new key ID. The "key add" command creates a new key and validates the key. Returns the new key ID.
EXIT STATUS EXIT STATUS
=========== ===========
@@ -30,7 +33,7 @@ Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runKeyAdd(cmd.Context(), globalOptions, opts, args) return runKeyAdd(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
}, },
} }
@@ -52,21 +55,22 @@ func (opts *KeyAddOptions) Add(flags *pflag.FlagSet) {
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key") flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
} }
func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error { func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, args []string, term ui.Terminal) error {
if len(args) > 0 { if len(args) > 0 {
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags") return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")
} }
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false) printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false, printer)
if err != nil { if err != nil {
return err return err
} }
defer unlock() defer unlock()
return addKey(ctx, repo, gopts, opts) return addKey(ctx, repo, gopts, opts, printer)
} }
func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyAddOptions) error { func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer progress.Printer) error {
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword) pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
if err != nil { if err != nil {
return err return err
@@ -74,7 +78,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key()) id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key())
if err != nil { if err != nil {
return errors.Fatalf("creating new key failed: %v\n", err) return errors.Fatalf("creating new key failed: %v", err)
} }
err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw) err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw)
@@ -82,7 +86,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
return err return err
} }
Verbosef("saved new key with ID %s\n", id.ID()) printer.P("saved new key with ID %s", id.ID())
return nil return nil
} }
@@ -90,7 +94,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
// testKeyNewPassword is used to set a new password during integration testing. // testKeyNewPassword is used to set a new password during integration testing.
var testKeyNewPassword string var testKeyNewPassword string
func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string, insecureNoPassword bool) (string, error) { func getNewPassword(ctx context.Context, gopts global.Options, newPasswordFile string, insecureNoPassword bool) (string, error) {
if testKeyNewPassword != "" { if testKeyNewPassword != "" {
return testKeyNewPassword, nil return testKeyNewPassword, nil
} }
@@ -103,7 +107,7 @@ func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile st
} }
if newPasswordFile != "" { if newPasswordFile != "" {
password, err := loadPasswordFromFile(newPasswordFile) password, err := global.LoadPasswordFromFile(newPasswordFile)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -116,11 +120,11 @@ func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile st
// Since we already have an open repository, temporary remove the password // Since we already have an open repository, temporary remove the password
// to prompt the user for the passwd. // to prompt the user for the passwd.
newopts := gopts newopts := gopts
newopts.password = "" newopts.Password = ""
// empty passwords are already handled above // empty passwords are already handled above
newopts.InsecureNoPassword = false newopts.InsecureNoPassword = false
return ReadPasswordTwice(ctx, newopts, return global.ReadPasswordTwice(ctx, newopts,
"enter new password: ", "enter new password: ",
"enter password again: ") "enter password again: ")
} }
+126 -50
View File
@@ -10,13 +10,15 @@ import (
"testing" "testing"
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/progress"
) )
func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string { func testRunKeyListOtherIDs(t testing.TB, gopts global.Options) []string {
buf, err := withCaptureStdout(func() error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyList(context.TODO(), gopts, []string{}) return runKeyList(ctx, gopts, []string{}, gopts.Term)
}) })
rtest.OK(t, err) rtest.OK(t, err)
@@ -33,49 +35,94 @@ func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
return IDs return IDs
} }
func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions) { func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts global.Options) {
testKeyNewPassword = newPassword testKeyNewPassword = newPassword
defer func() { defer func() {
testKeyNewPassword = "" testKeyNewPassword = ""
}() }()
rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{}, []string{})) err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.Term)
})
rtest.OK(t, err)
} }
func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) { func testRunKeyAddNewKeyUserHost(t testing.TB, gopts global.Options) {
testKeyNewPassword = "john's geheimnis" testKeyNewPassword = "john's geheimnis"
defer func() { defer func() {
testKeyNewPassword = "" testKeyNewPassword = ""
}() }()
t.Log("adding key for john@example.com") t.Log("adding key for john@example.com")
rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{ err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
Username: "john", return runKeyAdd(ctx, gopts, KeyAddOptions{
Hostname: "example.com", Username: "john",
}, []string{})) Hostname: "example.com",
}, []string{}, gopts.Term)
repo, err := OpenRepository(context.TODO(), gopts) })
rtest.OK(t, err)
key, err := repository.SearchKey(context.TODO(), repo, testKeyNewPassword, 2, "")
rtest.OK(t, err) rtest.OK(t, err)
rtest.Equals(t, "john", key.Username) _ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
rtest.Equals(t, "example.com", key.Hostname) repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
rtest.OK(t, err)
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 2, "")
rtest.OK(t, err)
rtest.Equals(t, "john", key.Username)
rtest.Equals(t, "example.com", key.Hostname)
return nil
})
} }
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) { func testRunKeyPasswd(t testing.TB, newPassword string, gopts global.Options) {
testKeyNewPassword = newPassword testKeyNewPassword = newPassword
defer func() { defer func() {
testKeyNewPassword = "" testKeyNewPassword = ""
}() }()
rtest.OK(t, runKeyPasswd(context.TODO(), gopts, KeyPasswdOptions{}, []string{})) err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.Term)
})
rtest.OK(t, err)
} }
func testRunKeyRemove(t testing.TB, gopts GlobalOptions, IDs []string) { func testRunKeyPasswdUserHost(t testing.TB, newPassword string, gopts global.Options) {
testKeyNewPassword = newPassword
defer func() {
testKeyNewPassword = ""
}()
t.Log("changing password and setting key for john@example.com")
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{
KeyAddOptions: KeyAddOptions{
Username: "john",
Hostname: "example.com",
},
}, []string{}, gopts.Term)
})
rtest.OK(t, err)
gopts.Password = testKeyNewPassword
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
rtest.OK(t, err)
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 1, "")
rtest.OK(t, err)
rtest.Equals(t, "john", key.Username)
rtest.Equals(t, "example.com", key.Hostname)
return nil
})
}
func testRunKeyRemove(t testing.TB, gopts global.Options, IDs []string) {
t.Logf("remove %d keys: %q\n", len(IDs), IDs) t.Logf("remove %d keys: %q\n", len(IDs), IDs)
for _, id := range IDs { for _, id := range IDs {
rtest.OK(t, runKeyRemove(context.TODO(), gopts, []string{id})) err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyRemove(ctx, gopts, []string{id}, gopts.Term)
})
rtest.OK(t, err)
} }
} }
@@ -87,25 +134,30 @@ func TestKeyAddRemove(t *testing.T) {
env, cleanup := withTestEnvironment(t) env, cleanup := withTestEnvironment(t)
// must list keys more than once // must list keys more than once
env.gopts.backendTestHook = nil env.gopts.BackendTestHook = nil
defer cleanup() defer cleanup()
testRunInit(t, env.gopts) testRunInit(t, env.gopts)
testRunKeyPasswd(t, "geheim2", env.gopts) testRunKeyPasswd(t, "geheim2", env.gopts)
env.gopts.password = "geheim2" env.gopts.Password = "geheim2"
t.Logf("changed password to %q", env.gopts.password) testRunKeyPasswdUserHost(t, "geheim3", env.gopts)
env.gopts.Password = "geheim3"
t.Logf("changed password to %q", env.gopts.Password)
for _, newPassword := range passwordList { for _, newPassword := range passwordList {
testRunKeyAddNewKey(t, newPassword, env.gopts) testRunKeyAddNewKey(t, newPassword, env.gopts)
t.Logf("added new password %q", newPassword) t.Logf("added new password %q", newPassword)
env.gopts.password = newPassword env.gopts.Password = newPassword
testRunKeyRemove(t, env.gopts, testRunKeyListOtherIDs(t, env.gopts)) testRunKeyRemove(t, env.gopts, testRunKeyListOtherIDs(t, env.gopts))
} }
env.gopts.password = passwordList[len(passwordList)-1] env.gopts.Password = passwordList[len(passwordList)-1]
t.Logf("testing access with last password %q\n", env.gopts.password) t.Logf("testing access with last password %q\n", env.gopts.Password)
rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{})) err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyList(ctx, gopts, []string{}, gopts.Term)
})
rtest.OK(t, err)
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
testRunKeyAddNewKeyUserHost(t, env.gopts) testRunKeyAddNewKeyUserHost(t, env.gopts)
@@ -116,33 +168,40 @@ func TestKeyAddInvalid(t *testing.T) {
defer cleanup() defer cleanup()
testRunInit(t, env.gopts) testRunInit(t, env.gopts)
err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{ err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
NewPasswordFile: "some-file", return runKeyAdd(ctx, gopts, KeyAddOptions{
InsecureNoPassword: true, NewPasswordFile: "some-file",
}, []string{}) InsecureNoPassword: true,
}, []string{}, gopts.Term)
})
rtest.Assert(t, strings.Contains(err.Error(), "only either"), "unexpected error message, got %q", err) rtest.Assert(t, strings.Contains(err.Error(), "only either"), "unexpected error message, got %q", err)
pwfile := filepath.Join(t.TempDir(), "pwfile") pwfile := filepath.Join(t.TempDir(), "pwfile")
rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666)) rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666))
err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{ err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
NewPasswordFile: pwfile, return runKeyAdd(ctx, gopts, KeyAddOptions{
}, []string{}) NewPasswordFile: pwfile,
}, []string{}, gopts.Term)
})
rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err) rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err)
} }
func TestKeyAddEmpty(t *testing.T) { func TestKeyAddEmpty(t *testing.T) {
env, cleanup := withTestEnvironment(t) env, cleanup := withTestEnvironment(t)
// must list keys more than once // must list keys more than once
env.gopts.backendTestHook = nil env.gopts.BackendTestHook = nil
defer cleanup() defer cleanup()
testRunInit(t, env.gopts) testRunInit(t, env.gopts)
rtest.OK(t, runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{ err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
InsecureNoPassword: true, return runKeyAdd(ctx, gopts, KeyAddOptions{
}, []string{})) InsecureNoPassword: true,
}, []string{}, gopts.Term)
})
rtest.OK(t, err)
env.gopts.password = "" env.gopts.Password = ""
env.gopts.InsecureNoPassword = true env.gopts.InsecureNoPassword = true
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
@@ -161,7 +220,7 @@ func TestKeyProblems(t *testing.T) {
defer cleanup() defer cleanup()
testRunInit(t, env.gopts) testRunInit(t, env.gopts)
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) { env.gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) {
return &emptySaveBackend{r}, nil return &emptySaveBackend{r}, nil
} }
@@ -170,16 +229,23 @@ func TestKeyProblems(t *testing.T) {
testKeyNewPassword = "" testKeyNewPassword = ""
}() }()
err := runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{}) err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.Term)
})
t.Log(err) t.Log(err)
rtest.Assert(t, err != nil, "expected passwd change to fail") rtest.Assert(t, err != nil, "expected passwd change to fail")
err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{}) err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.Term)
})
t.Log(err) t.Log(err)
rtest.Assert(t, err != nil, "expected key adding to fail") rtest.Assert(t, err != nil, "expected key adding to fail")
t.Logf("testing access with initial password %q\n", env.gopts.password) t.Logf("testing access with initial password %q\n", env.gopts.Password)
rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{})) err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyList(ctx, gopts, []string{}, gopts.Term)
})
rtest.OK(t, err)
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
} }
@@ -188,27 +254,37 @@ func TestKeyCommandInvalidArguments(t *testing.T) {
defer cleanup() defer cleanup()
testRunInit(t, env.gopts) testRunInit(t, env.gopts)
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) { env.gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) {
return &emptySaveBackend{r}, nil return &emptySaveBackend{r}, nil
} }
err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{"johndoe"}) err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{"johndoe"}, gopts.Term)
})
t.Log(err) t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key add: %v", err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key add: %v", err)
err = runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{"johndoe"}) err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{"johndoe"}, gopts.Term)
})
t.Log(err) t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key passwd: %v", err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key passwd: %v", err)
err = runKeyList(context.TODO(), env.gopts, []string{"johndoe"}) err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyList(ctx, gopts, []string{"johndoe"}, gopts.Term)
})
t.Log(err) t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key list: %v", err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key list: %v", err)
err = runKeyRemove(context.TODO(), env.gopts, []string{}) err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyRemove(ctx, gopts, []string{}, gopts.Term)
})
t.Log(err) t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
err = runKeyRemove(context.TODO(), env.gopts, []string{"john", "doe"}) err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
return runKeyRemove(ctx, gopts, []string{"john", "doe"}, gopts.Term)
})
t.Log(err) t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
} }
+15 -11
View File
@@ -6,18 +6,21 @@ import (
"fmt" "fmt"
"sync" "sync"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "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/restic/restic/internal/ui/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func newKeyListCommand() *cobra.Command { func newKeyListCommand(globalOptions *global.Options) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "List keys (passwords)", Short: "List keys (passwords)",
Long: ` Long: `
The "list" sub-command lists all the keys (passwords) associated with the repository. The "key list" command lists all the keys (passwords) associated with the repository.
Returns the key ID, username, hostname, created time and if it's the current key being Returns the key ID, username, hostname, created time and if it's the current key being
used to access the repository. used to access the repository.
@@ -32,27 +35,28 @@ Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runKeyList(cmd.Context(), globalOptions, args) return runKeyList(cmd.Context(), *globalOptions, args, globalOptions.Term)
}, },
} }
return cmd return cmd
} }
func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error { func runKeyList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
if len(args) > 0 { if len(args) > 0 {
return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags") return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags")
} }
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil { if err != nil {
return err return err
} }
defer unlock() defer unlock()
return listKeys(ctx, repo, gopts) return listKeys(ctx, repo, gopts, printer)
} }
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error { func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer progress.Printer) error {
type keyInfo struct { type keyInfo struct {
Current bool `json:"current"` Current bool `json:"current"`
ID string `json:"id"` ID string `json:"id"`
@@ -68,7 +72,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
err := restic.ParallelList(ctx, s, restic.KeyFile, s.Connections(), func(ctx context.Context, id restic.ID, _ int64) error { err := restic.ParallelList(ctx, s, restic.KeyFile, s.Connections(), func(ctx context.Context, id restic.ID, _ int64) error {
k, err := repository.LoadKey(ctx, s, id) k, err := repository.LoadKey(ctx, s, id)
if err != nil { if err != nil {
Warnf("LoadKey() failed: %v\n", err) printer.E("LoadKey() failed: %v", err)
return nil return nil
} }
@@ -78,7 +82,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
ShortID: id.Str(), ShortID: id.Str(),
UserName: k.Username, UserName: k.Username,
HostName: k.Hostname, HostName: k.Hostname,
Created: k.Created.Local().Format(TimeFormat), Created: k.Created.Local().Format(global.TimeFormat),
} }
m.Lock() m.Lock()
@@ -92,7 +96,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
} }
if gopts.JSON { if gopts.JSON {
return json.NewEncoder(globalOptions.stdout).Encode(keys) return json.NewEncoder(gopts.Term.OutputWriter()).Encode(keys)
} }
tab := table.New() tab := table.New()
@@ -105,5 +109,5 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
tab.AddRow(key) tab.AddRow(key)
} }
return tab.Write(globalOptions.stdout) return tab.Write(gopts.Term.OutputWriter())
} }
+15 -11
View File
@@ -5,20 +5,23 @@ import (
"fmt" "fmt"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "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/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func newKeyPasswdCommand() *cobra.Command { func newKeyPasswdCommand(globalOptions *global.Options) *cobra.Command {
var opts KeyPasswdOptions var opts KeyPasswdOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "passwd", Use: "passwd",
Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID", Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID",
Long: ` Long: `
The "passwd" sub-command creates a new key, validates the key and remove the old key ID. The "key passwd" command creates a new key, validates the key and removes the old key ID.
Returns the new key ID. Returns the new key ID.
EXIT STATUS EXIT STATUS
=========== ===========
@@ -31,7 +34,7 @@ Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runKeyPasswd(cmd.Context(), globalOptions, opts, args) return runKeyPasswd(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
}, },
} }
@@ -47,29 +50,30 @@ func (opts *KeyPasswdOptions) AddFlags(flags *pflag.FlagSet) {
opts.KeyAddOptions.Add(flags) opts.KeyAddOptions.Add(flags)
} }
func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error { func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptions, args []string, term ui.Terminal) error {
if len(args) > 0 { if len(args) > 0 {
return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags") return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags")
} }
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil { if err != nil {
return err return err
} }
defer unlock() defer unlock()
return changePassword(ctx, repo, gopts, opts) return changePassword(ctx, repo, gopts, opts, printer)
} }
func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyPasswdOptions) error { func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer progress.Printer) error {
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword) pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
if err != nil { if err != nil {
return err return err
} }
id, err := repository.AddKey(ctx, repo, pw, "", "", repo.Key()) id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key())
if err != nil { if err != nil {
return errors.Fatalf("creating new key failed: %v\n", err) return errors.Fatalf("creating new key failed: %v", err)
} }
oldID := repo.KeyID() oldID := repo.KeyID()
@@ -83,7 +87,7 @@ func changePassword(ctx context.Context, repo *repository.Repository, gopts Glob
return err return err
} }
Verbosef("saved new key as %s\n", id) printer.P("saved new key as %s", id)
return nil return nil
} }
+14 -10
View File
@@ -5,18 +5,21 @@ import (
"fmt" "fmt"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "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/cobra"
) )
func newKeyRemoveCommand() *cobra.Command { func newKeyRemoveCommand(globalOptions *global.Options) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "remove [ID]", Use: "remove [ID]",
Short: "Remove key ID (password) from the repository.", Short: "Remove key ID (password) from the repository",
Long: ` Long: `
The "remove" sub-command removes the selected key ID. The "remove" command does not allow The "key remove" command removes the selected key ID. It does not allow removing the
removing the current key being used to access the repository. current key being used to access the repository.
EXIT STATUS EXIT STATUS
=========== ===========
@@ -29,27 +32,28 @@ Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runKeyRemove(cmd.Context(), globalOptions, args) return runKeyRemove(cmd.Context(), *globalOptions, args, globalOptions.Term)
}, },
} }
return cmd return cmd
} }
func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string) error { func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
if len(args) != 1 { if len(args) != 1 {
return fmt.Errorf("key remove expects one argument as the key id") return fmt.Errorf("key remove expects one argument as the key id")
} }
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil { if err != nil {
return err return err
} }
defer unlock() defer unlock()
return deleteKey(ctx, repo, args[0]) return deleteKey(ctx, repo, args[0], printer)
} }
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string) error { func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer progress.Printer) error {
id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix) id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix)
if err != nil { if err != nil {
return err return err
@@ -64,6 +68,6 @@ func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string
return err return err
} }
Verbosef("removed key %v\n", id) printer.P("removed key %v", id)
return nil return nil
} }
+16 -8
View File
@@ -5,13 +5,15 @@ import (
"strings" "strings"
"github.com/restic/restic/internal/errors" "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/index"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func newListCommand() *cobra.Command { func newListCommand(globalOptions *global.Options) *cobra.Command {
var listAllowedArgs = []string{"blobs", "packs", "index", "snapshots", "keys", "locks"} var listAllowedArgs = []string{"blobs", "packs", "index", "snapshots", "keys", "locks"}
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|") var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
@@ -33,7 +35,7 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true, DisableAutoGenTag: true,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), globalOptions, args) return runList(cmd.Context(), *globalOptions, args, globalOptions.Term)
}, },
ValidArgs: listAllowedArgs, ValidArgs: listAllowedArgs,
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
@@ -41,12 +43,14 @@ Exit status is 12 if the password is incorrect.
return cmd return cmd
} }
func runList(ctx context.Context, gopts GlobalOptions, args []string) error { func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
if len(args) != 1 { if len(args) != 1 {
return errors.Fatal("type not specified") return errors.Fatal("type not specified")
} }
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks") ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks", printer)
if err != nil { if err != nil {
return err return err
} }
@@ -69,16 +73,20 @@ func runList(ctx context.Context, gopts GlobalOptions, args []string) error {
if err != nil { if err != nil {
return err return err
} }
return idx.Each(ctx, func(blobs restic.PackedBlob) { for blobs := range idx.Values() {
Printf("%v %v\n", blobs.Type, blobs.ID) if ctx.Err() != nil {
}) return ctx.Err()
}
printer.S("%v %v", blobs.Type, blobs.ID)
}
return nil
}) })
default: default:
return errors.Fatal("invalid type") return errors.Fatal("invalid type")
} }
return repo.List(ctx, t, func(id restic.ID, _ int64) error { return repo.List(ctx, t, func(id restic.ID, _ int64) error {
Printf("%s\n", id) printer.S("%s", id)
return nil return nil
}) })
} }
+71 -11
View File
@@ -4,15 +4,19 @@ import (
"bufio" "bufio"
"context" "context"
"io" "io"
"path/filepath"
"strings"
"testing" "testing"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
) )
func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs { func testRunList(t testing.TB, gopts global.Options, tpe string) restic.IDs {
buf, err := withCaptureStdout(func() error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runList(context.TODO(), opts, []string{tpe}) return runList(ctx, gopts, []string{tpe}, gopts.Term)
}) })
rtest.OK(t, err) rtest.OK(t, err)
return parseIDsFromReader(t, buf) return parseIDsFromReader(t, buf)
@@ -24,21 +28,77 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs {
sc := bufio.NewScanner(rd) sc := bufio.NewScanner(rd)
for sc.Scan() { for sc.Scan() {
id, err := restic.ParseID(sc.Text()) if len(sc.Text()) == 64 {
if err != nil { id, err := restic.ParseID(sc.Text())
t.Logf("parse id %v: %v", sc.Text(), err) if err != nil {
continue t.Logf("parse id %v: %v", sc.Text(), err)
continue
}
IDs = append(IDs, id)
} else {
// 'list blobs' is different because it lists the blobs together with the blob type
// e.g. "tree ac08ce34ba4f8123618661bef2425f7028ffb9ac740578a3ee88684d2523fee8"
parts := strings.Split(sc.Text(), " ")
id, err := restic.ParseID(parts[len(parts)-1])
if err != nil {
t.Logf("parse id %v: %v", sc.Text(), err)
continue
}
IDs = append(IDs, id)
} }
IDs = append(IDs, id)
} }
return IDs return IDs
} }
func testListSnapshots(t testing.TB, opts GlobalOptions, expected int) restic.IDs { func testListSnapshots(t testing.TB, gopts global.Options, expected int) restic.IDs {
t.Helper() t.Helper()
snapshotIDs := testRunList(t, "snapshots", opts) snapshotIDs := testRunList(t, gopts, "snapshots")
rtest.Assert(t, len(snapshotIDs) == expected, "expected %v snapshot, got %v", expected, snapshotIDs) rtest.Assert(t, len(snapshotIDs) == expected, "expected %v snapshot, got %v", expected, snapshotIDs)
return snapshotIDs return snapshotIDs
} }
// 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)
_, 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))
// get blobs from index
blobSetFromIndex = restic.NewIDSet()
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
blobSetFromIndex.Insert(blob.ID)
}))
return nil
})
rtest.OK(t, err)
return blobSetFromIndex
}
func TestListBlobs(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
opts := BackupOptions{}
// first backup
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
testListSnapshots(t, env.gopts, 1)
// run the `list blobs` command
resticIDs := testRunList(t, env.gopts, "blobs")
// convert to set
testIDSet := restic.NewIDSet(resticIDs...)
blobSetFromIndex := testListBlobs(t, env.gopts)
rtest.Assert(t, blobSetFromIndex.Equals(testIDSet), "the set of restic.ID s should be equal")
}
+42 -36
View File
@@ -15,13 +15,16 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/global"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
) )
func newLsCommand() *cobra.Command { func newLsCommand(globalOptions *global.Options) *cobra.Command {
var opts LsOptions var opts LsOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -59,7 +62,8 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true, DisableAutoGenTag: true,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runLs(cmd.Context(), opts, globalOptions, args) finalizeSnapshotFilter(&opts.SnapshotFilter)
return runLs(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
}, },
} }
opts.AddFlags(cmd.Flags()) opts.AddFlags(cmd.Flags())
@@ -69,7 +73,7 @@ Exit status is 12 if the password is incorrect.
// LsOptions collects all options for the ls command. // LsOptions collects all options for the ls command.
type LsOptions struct { type LsOptions struct {
ListLong bool ListLong bool
restic.SnapshotFilter data.SnapshotFilter
Recursive bool Recursive bool
HumanReadable bool HumanReadable bool
Ncdu bool Ncdu bool
@@ -88,8 +92,8 @@ func (opts *LsOptions) AddFlags(f *pflag.FlagSet) {
} }
type lsPrinter interface { type lsPrinter interface {
Snapshot(sn *restic.Snapshot) error Snapshot(sn *data.Snapshot) error
Node(path string, node *restic.Node, isPrefixDirectory bool) error Node(path string, node *data.Node, isPrefixDirectory bool) error
LeaveDir(path string) error LeaveDir(path string) error
Close() error Close() error
} }
@@ -98,9 +102,9 @@ type jsonLsPrinter struct {
enc *json.Encoder enc *json.Encoder
} }
func (p *jsonLsPrinter) Snapshot(sn *restic.Snapshot) error { func (p *jsonLsPrinter) Snapshot(sn *data.Snapshot) error {
type lsSnapshot struct { type lsSnapshot struct {
*restic.Snapshot *data.Snapshot
ID *restic.ID `json:"id"` ID *restic.ID `json:"id"`
ShortID string `json:"short_id"` // deprecated ShortID string `json:"short_id"` // deprecated
MessageType string `json:"message_type"` // "snapshot" MessageType string `json:"message_type"` // "snapshot"
@@ -117,14 +121,14 @@ func (p *jsonLsPrinter) Snapshot(sn *restic.Snapshot) error {
} }
// Node formats node in our custom JSON format, followed by a newline. // Node formats node in our custom JSON format, followed by a newline.
func (p *jsonLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error { func (p *jsonLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
if isPrefixDirectory { if isPrefixDirectory {
return nil return nil
} }
return lsNodeJSON(p.enc, path, node) return lsNodeJSON(p.enc, path, node)
} }
func lsNodeJSON(enc *json.Encoder, path string, node *restic.Node) error { func lsNodeJSON(enc *json.Encoder, path string, node *data.Node) error {
n := &struct { n := &struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
@@ -160,7 +164,7 @@ func lsNodeJSON(enc *json.Encoder, path string, node *restic.Node) error {
} }
// Always print size for regular files, even when empty, // Always print size for regular files, even when empty,
// but never for other types. // but never for other types.
if node.Type == restic.NodeTypeFile { if node.Type == data.NodeTypeFile {
n.Size = &n.size n.Size = &n.size
} }
@@ -178,7 +182,7 @@ type ncduLsPrinter struct {
// Snapshot prints a restic snapshot in Ncdu save format. // Snapshot prints a restic snapshot in Ncdu save format.
// It opens the JSON list. Nodes are added with lsNodeNcdu and the list is closed by lsCloseNcdu. // It opens the JSON list. Nodes are added with lsNodeNcdu and the list is closed by lsCloseNcdu.
// Format documentation: https://dev.yorhel.nl/ncdu/jsonfmt // Format documentation: https://dev.yorhel.nl/ncdu/jsonfmt
func (p *ncduLsPrinter) Snapshot(sn *restic.Snapshot) error { func (p *ncduLsPrinter) Snapshot(sn *data.Snapshot) error {
const NcduMajorVer = 1 const NcduMajorVer = 1
const NcduMinorVer = 2 const NcduMinorVer = 2
@@ -191,7 +195,7 @@ func (p *ncduLsPrinter) Snapshot(sn *restic.Snapshot) error {
return err return err
} }
func lsNcduNode(_ string, node *restic.Node) ([]byte, error) { func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
type NcduNode struct { type NcduNode struct {
Name string `json:"name"` Name string `json:"name"`
Asize uint64 `json:"asize"` Asize uint64 `json:"asize"`
@@ -216,7 +220,7 @@ func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
Dev: node.DeviceID, Dev: node.DeviceID,
Ino: node.Inode, Ino: node.Inode,
NLink: node.Links, NLink: node.Links,
NotReg: node.Type != restic.NodeTypeDir && node.Type != restic.NodeTypeFile, NotReg: node.Type != data.NodeTypeDir && node.Type != data.NodeTypeFile,
UID: node.UID, UID: node.UID,
GID: node.GID, GID: node.GID,
Mode: uint16(node.Mode & os.ModePerm), Mode: uint16(node.Mode & os.ModePerm),
@@ -240,13 +244,13 @@ func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
return json.Marshal(outNode) return json.Marshal(outNode)
} }
func (p *ncduLsPrinter) Node(path string, node *restic.Node, _ bool) error { func (p *ncduLsPrinter) Node(path string, node *data.Node, _ bool) error {
out, err := lsNcduNode(path, node) out, err := lsNcduNode(path, node)
if err != nil { if err != nil {
return err return err
} }
if node.Type == restic.NodeTypeDir { if node.Type == data.NodeTypeDir {
_, err = fmt.Fprintf(p.out, ",\n%s[\n%s%s", strings.Repeat(" ", p.depth), strings.Repeat(" ", p.depth+1), string(out)) _, err = fmt.Fprintf(p.out, ",\n%s[\n%s%s", strings.Repeat(" ", p.depth), strings.Repeat(" ", p.depth+1), string(out))
p.depth++ p.depth++
} else { } else {
@@ -270,15 +274,19 @@ type textLsPrinter struct {
dirs []string dirs []string
ListLong bool ListLong bool
HumanReadable bool HumanReadable bool
termPrinter interface {
P(msg string, args ...interface{})
S(msg string, args ...interface{})
}
} }
func (p *textLsPrinter) Snapshot(sn *restic.Snapshot) error { func (p *textLsPrinter) Snapshot(sn *data.Snapshot) error {
Verbosef("%v filtered by %v:\n", sn, p.dirs) p.termPrinter.P("%v filtered by %v:", sn, p.dirs)
return nil return nil
} }
func (p *textLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error { func (p *textLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
if !isPrefixDirectory { if !isPrefixDirectory {
Printf("%s\n", formatNode(path, node, p.ListLong, p.HumanReadable)) p.termPrinter.S("%s", formatNode(path, node, p.ListLong, p.HumanReadable))
} }
return nil return nil
} }
@@ -293,10 +301,12 @@ func (p *textLsPrinter) Close() error {
// for ls -l output sorting // for ls -l output sorting
type toSortOutput struct { type toSortOutput struct {
nodepath string nodepath string
node *restic.Node node *data.Node
} }
func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []string) error { func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []string, term ui.Terminal) error {
termPrinter := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
if len(args) == 0 { if len(args) == 0 {
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'") return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
} }
@@ -355,7 +365,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
return false return false
} }
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, termPrinter)
if err != nil { if err != nil {
return err return err
} }
@@ -366,8 +376,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
return err return err
} }
bar := newIndexProgress(gopts.Quiet, gopts.JSON) if err = repo.LoadIndex(ctx, termPrinter); err != nil {
if err = repo.LoadIndex(ctx, bar); err != nil {
return err return err
} }
@@ -375,17 +384,18 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
if gopts.JSON { if gopts.JSON {
printer = &jsonLsPrinter{ printer = &jsonLsPrinter{
enc: json.NewEncoder(globalOptions.stdout), enc: json.NewEncoder(gopts.Term.OutputWriter()),
} }
} else if opts.Ncdu { } else if opts.Ncdu {
printer = &ncduLsPrinter{ printer = &ncduLsPrinter{
out: globalOptions.stdout, out: gopts.Term.OutputWriter(),
} }
} else { } else {
printer = &textLsPrinter{ printer = &textLsPrinter{
dirs: dirs, dirs: dirs,
ListLong: opts.ListLong, ListLong: opts.ListLong,
HumanReadable: opts.HumanReadable, HumanReadable: opts.HumanReadable,
termPrinter: termPrinter,
} }
} }
if opts.Sort != SortModeName || opts.Reverse { if opts.Sort != SortModeName || opts.Reverse {
@@ -396,16 +406,12 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
} }
} }
sn, subfolder, err := (&restic.SnapshotFilter{ sn, subfolder, err := opts.SnapshotFilter.FindLatest(ctx, snapshotLister, repo, args[0])
Hosts: opts.Hosts,
Paths: opts.Paths,
Tags: opts.Tags,
}).FindLatest(ctx, snapshotLister, repo, args[0])
if err != nil { if err != nil {
return err return err
} }
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subfolder) sn.Tree, err = data.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
if err != nil { if err != nil {
return err return err
} }
@@ -414,7 +420,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
return err return err
} }
processNode := func(_ restic.ID, nodepath string, node *restic.Node, err error) error { processNode := func(_ restic.ID, nodepath string, node *data.Node, err error) error {
if err != nil { if err != nil {
return err return err
} }
@@ -449,7 +455,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
// otherwise, signal the walker to not walk recursively into any // otherwise, signal the walker to not walk recursively into any
// subdirs // subdirs
if node.Type == restic.NodeTypeDir { if node.Type == data.NodeTypeDir {
// immediately generate leaveDir if the directory is skipped // immediately generate leaveDir if the directory is skipped
if printedDir { if printedDir {
if err := printer.LeaveDir(nodepath); err != nil { if err := printer.LeaveDir(nodepath); err != nil {
@@ -486,10 +492,10 @@ type sortedPrinter struct {
reverse bool reverse bool
} }
func (p *sortedPrinter) Snapshot(sn *restic.Snapshot) error { func (p *sortedPrinter) Snapshot(sn *data.Snapshot) error {
return p.printer.Snapshot(sn) return p.printer.Snapshot(sn)
} }
func (p *sortedPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error { func (p *sortedPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
if !isPrefixDirectory { if !isPrefixDirectory {
p.collector = append(p.collector, toSortOutput{path, node}) p.collector = append(p.collector, toSortOutput{path, node})
} }

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