mirror of
https://github.com/restic/restic.git
synced 2026-09-14 08:17:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfd94e9348 | ||
|
|
ba802d42b7 | ||
|
|
46ea365629 | ||
|
|
fb5e9df357 | ||
|
|
b593ba305b | ||
|
|
28d802df8c | ||
|
|
0946f45de6 | ||
|
|
5895e6d059 | ||
|
|
e982b6e7b5 | ||
|
|
479cf1fb3d | ||
|
|
dbaa8cf124 | ||
|
|
a80be1478a | ||
|
|
7bfa32a90a | ||
|
|
63b583c4ea | ||
|
|
85bf4430b8 | ||
|
|
e2b43a5f1f | ||
|
|
607aba3809 | ||
|
|
c6074f82fb | ||
|
|
8baffc4027 | ||
|
|
f215365af9 | ||
|
|
14e523a1d9 | ||
|
|
58f92708c2 | ||
|
|
31e96f4ec9 | ||
|
|
a2cb015591 | ||
|
|
e2495b72bb | ||
|
|
2c169edad4 | ||
|
|
fb36a7f59b | ||
|
|
8ed263c6dd | ||
|
|
0e4f288e83 | ||
|
|
da4d8f5851 | ||
|
|
32e9869cc8 | ||
|
|
d4088aa09b | ||
|
|
35dcda418c | ||
|
|
af4f50223e | ||
|
|
9e007fa3cc | ||
|
|
a4d85ce299 | ||
|
|
9b0ee37697 | ||
|
|
905ca56ee8 | ||
|
|
987caba408 | ||
|
|
bfca18c1b0 | ||
|
|
dba493cb6c | ||
|
|
512d0577d6 | ||
|
|
e428de5f84 | ||
|
|
d8ef26afa4 | ||
|
|
cc93a94e15 | ||
|
|
62991338ea | ||
|
|
75de8b54e6 | ||
|
|
e4056e70a8 | ||
|
|
8cc69782b5 | ||
|
|
ccb828e87b | ||
|
|
70a45791b2 | ||
|
|
9e1a526611 | ||
|
|
aa94be2dbd | ||
|
|
804ff4bc95 | ||
|
|
9670cd7459 | ||
|
|
634e9b2411 | ||
|
|
db03aed816 | ||
|
|
4e8eea7ab4 | ||
|
|
981adc47ac | ||
|
|
7134577c56 | ||
|
|
55e335ec6c | ||
|
|
d30d6b6281 | ||
|
|
8d7679c831 | ||
|
|
d9dd1b7534 | ||
|
|
1151d70655 | ||
|
|
369da8a770 | ||
|
|
9f84c80056 | ||
|
|
6070462465 | ||
|
|
1029fe6dd0 | ||
|
|
8f00b335b4 | ||
|
|
c6a064b68b | ||
|
|
8d191c19a5 | ||
|
|
d5267f03e0 | ||
|
|
5683224a3c | ||
|
|
71064fac10 | ||
|
|
52cde35aea | ||
|
|
f394723aa2 | ||
|
|
072305c5b3 | ||
|
|
a1420ea6c7 | ||
|
|
a3be3bd9e1 | ||
|
|
b24d05bcb4 | ||
|
|
c021b8cde0 | ||
|
|
18e5c446e0 | ||
|
|
f91afb2fa8 | ||
|
|
2148508050 | ||
|
|
37e8f50aeb | ||
|
|
7e5eaf1ead | ||
|
|
aae0985673 | ||
|
|
f9d953d4cd | ||
|
|
69a3ba5b33 | ||
|
|
c183fe2035 | ||
|
|
1174e97b30 | ||
|
|
440d925467 | ||
|
|
28521d27c1 | ||
|
|
af64caf5d9 | ||
|
|
9ab5fc59c2 | ||
|
|
6c509f7ac1 | ||
|
|
ea699bb0ba | ||
|
|
532f36a5a8 | ||
|
|
57ac5d107c | ||
|
|
2f31cde517 | ||
|
|
5cd39d01c1 | ||
|
|
61708772a2 | ||
|
|
d9d54a505e | ||
|
|
0f4236cb39 | ||
|
|
4d1b9cef63 | ||
|
|
284daaf0b4 | ||
|
|
980f194360 | ||
|
|
d795988fa8 | ||
|
|
203e595ccb | ||
|
|
6c67a095dc | ||
|
|
1a9decf03e | ||
|
|
718f09e02d | ||
|
|
82708ff7f7 | ||
|
|
18c47c5bf9 | ||
|
|
faea0d677a | ||
|
|
ae8fdb08fa | ||
|
|
5c8c048c1a | ||
|
|
6b88b072e0 | ||
|
|
7d36449ea8 | ||
|
|
71f38d266c | ||
|
|
0536e15bbf | ||
|
|
650f658244 | ||
|
|
ae96b2a0f8 | ||
|
|
ce9b6ceea1 | ||
|
|
406dec29a7 | ||
|
|
6fe10a72eb | ||
|
|
1a6d43286a | ||
|
|
151903d4d4 | ||
|
|
b86568b790 | ||
|
|
a5f3256626 | ||
|
|
5989671f87 | ||
|
|
b7f3a1367a | ||
|
|
4838b0ae3e | ||
|
|
c87da70af9 | ||
|
|
81b6414c55 | ||
|
|
637c1cfb66 | ||
|
|
b892b1a150 | ||
|
|
7015da44ad | ||
|
|
3ee465d363 | ||
|
|
8e11f5747d | ||
|
|
22a304af7e | ||
|
|
c8a0bc2c5e | ||
|
|
35e60dc2c7 | ||
|
|
8e93172bf8 | ||
|
|
b94ef4831d | ||
|
|
952b310d62 | ||
|
|
3eb5f83db3 | ||
|
|
d3ef21f870 | ||
|
|
31df7508c4 | ||
|
|
87c9ed4573 | ||
|
|
eedba12968 | ||
|
|
264cfbcff2 | ||
|
|
7f3c9eb749 | ||
|
|
347cac780d | ||
|
|
23d6946fc9 | ||
|
|
2db654533b | ||
|
|
f5843a7d97 | ||
|
|
e1e7fc8fb2 | ||
|
|
1acc19dac5 | ||
|
|
79e92b3bc6 | ||
|
|
766d9f9f1e | ||
|
|
131abbb4f0 | ||
|
|
d62e42e620 | ||
|
|
35caceace4 | ||
|
|
f2ff05f840 | ||
|
|
c062a78dcd | ||
|
|
35af104749 | ||
|
|
ccb5ae1592 | ||
|
|
97f1e99ed9 | ||
|
|
c060c317d3 | ||
|
|
a9e0b46358 | ||
|
|
8169814b38 | ||
|
|
f186e1e458 | ||
|
|
0938f52f38 | ||
|
|
9f9a3472aa | ||
|
|
14f86a462a | ||
|
|
825d67ba4b | ||
|
|
3b8b77e54d | ||
|
|
0bbfb072af | ||
|
|
e8ed2434cd | ||
|
|
c04f3e9d6c | ||
|
|
37fdbe5779 | ||
|
|
47328afb2e | ||
|
|
e6d054d896 | ||
|
|
fc81a0b6ad | ||
|
|
dee28f5b0e | ||
|
|
2d02a1ad39 | ||
|
|
bae49d00ee | ||
|
|
e5dba15367 | ||
|
|
bf56d71b09 | ||
|
|
e94ec65efb | ||
|
|
49f9e67520 | ||
|
|
3a4b0e3b8c | ||
|
|
abfc9bbdff | ||
|
|
784812361e | ||
|
|
c745f810b3 | ||
|
|
721411e04c | ||
|
|
b07ff6cd34 | ||
|
|
8b3bbb527c | ||
|
|
4a2d8e04aa | ||
|
|
a36488b706 | ||
|
|
cfb506a77a | ||
|
|
a15b1579ff | ||
|
|
ff95080f36 | ||
|
|
119bb9d9a8 | ||
|
|
8c6ee42d17 | ||
|
|
ccddc1914d | ||
|
|
4bc5eca7ea | ||
|
|
17ff3aa5f9 | ||
|
|
5683180a3b | ||
|
|
0fc7444e32 | ||
|
|
81571775d5 | ||
|
|
16b8b8cda0 | ||
|
|
7d25ca9d67 | ||
|
|
cc546b71e3 | ||
|
|
3cb49556f5 | ||
|
|
f625190393 | ||
|
|
620f5986f8 | ||
|
|
49c7364e79 | ||
|
|
e1e36ed848 | ||
|
|
a37010a825 | ||
|
|
a0d7745e8b |
@@ -22,6 +22,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
||||
|
||||
@@ -26,10 +26,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
name: size-label
|
||||
on: pull_request_target
|
||||
jobs:
|
||||
size-label:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: size-label
|
||||
uses: "pascalgn/size-label-action@56b489b027932ec0cf60438a1a5f1a19c8fc71ff" # v0.5.7
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -57,10 +57,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
@@ -222,10 +222,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go ${{ env.latest_go }}
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: ${{ env.latest_go }}
|
||||
|
||||
@@ -244,10 +244,10 @@ jobs:
|
||||
checks: write
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go ${{ env.latest_go }}
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: ${{ env.latest_go }}
|
||||
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
||||
+7
-1
@@ -32,7 +32,6 @@ linters:
|
||||
backend-imports:
|
||||
files:
|
||||
- "**/internal/backend/**"
|
||||
- "!**/internal/backend/cache/**"
|
||||
- "!**/internal/backend/test/**"
|
||||
- "!**/*_test.go"
|
||||
deny:
|
||||
@@ -40,6 +39,13 @@ linters:
|
||||
desc: "internal/restic should not be imported to keep the architectural layers intact"
|
||||
- pkg: "github.com/restic/restic/internal/repository"
|
||||
desc: "internal/repository should not be imported to keep the architectural layers intact"
|
||||
repository-internals:
|
||||
files:
|
||||
- "**"
|
||||
- "!**/internal/repository/**"
|
||||
deny:
|
||||
- pkg: "github.com/restic/restic/internal/repository/"
|
||||
desc: "packages below internal/repository should not be imported to not depend on repository internals"
|
||||
importas:
|
||||
alias:
|
||||
- pkg: github.com/restic/restic/internal/test
|
||||
|
||||
@@ -93,7 +93,7 @@ release. Instructions on how to do that are contained in the
|
||||
|
||||
## News
|
||||
|
||||
You can follow the restic project on Mastodon [@resticbackup](https://fosstodon.org/@restic) or subscribe to
|
||||
You can follow the restic project on Mastodon [@restic](https://fosstodon.org/@restic) or subscribe to
|
||||
the [project blog](https://restic.net/blog/).
|
||||
|
||||
## License
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Enhancement: show excluded items during backup in verbose mode
|
||||
|
||||
`restic backup -vv` now shows excluded items in verbose mode. Both text and JSON
|
||||
output are supported.
|
||||
|
||||
https://github.com/restic/restic/issues/21870
|
||||
https://github.com/restic/restic/pull/21887
|
||||
@@ -0,0 +1,11 @@
|
||||
Enhancement: `restic repair snapshots --forget` removes broken snapshots
|
||||
|
||||
In the past restic had no command to remove broken snapshot files.
|
||||
|
||||
`restic repair snapshots --forget` can now remove broken snapshot files,
|
||||
which have been found previously by `restic check` or by earlier error messages from restic commands.
|
||||
|
||||
For example, `restic repair snapshots --forget 1d204771` will remove a broken snapshot file with id `1d204771`.
|
||||
|
||||
https://github.com/restic/restic/issues/21892
|
||||
https://github.com/restic/restic/pull/21907
|
||||
@@ -0,0 +1,7 @@
|
||||
Enhancement: Add JSON support to prune
|
||||
|
||||
Restic `prune` now also supports the `--json` option and gives all
|
||||
statistics in JSON format.
|
||||
|
||||
https://github.com/restic/restic/issues/3129
|
||||
https://github.com/restic/restic/pull/5239
|
||||
@@ -0,0 +1,8 @@
|
||||
Enhancement: Add warmup support for check command on S3 backend
|
||||
|
||||
Like the other cold storage features, warmup support for the `check` command
|
||||
remains experimental and gated behind the "s3-restore" feature flag.
|
||||
|
||||
https://github.com/restic/restic/pull/5248
|
||||
https://github.com/restic/restic/issues/3202
|
||||
https://github.com/restic/restic/issues/2504
|
||||
@@ -0,0 +1,7 @@
|
||||
Enhancement: Add command to list packfiles belonging to a snapshot
|
||||
|
||||
This enhancement adds command `list packs snapshotID` to show the packfiles
|
||||
belonging to the given snapshot.
|
||||
|
||||
https://github.com/restic/restic/issues/5372
|
||||
https://github.com/restic/restic/pull/5396
|
||||
@@ -0,0 +1,7 @@
|
||||
Enhancement: `restic repair packs` can handle missing packfiles
|
||||
|
||||
Missing and damaged packfiles previously required separate approaches to fix a repository.
|
||||
Now, both cases can be handled by the `repair packs` command, which is now able to handle
|
||||
missing packfiles by removing them from the repository index.
|
||||
|
||||
https://github.com/restic/restic/pull/21845
|
||||
@@ -0,0 +1,6 @@
|
||||
Enhancement: `find --pack` can handle corrupted pack files
|
||||
|
||||
`restic find --pack <id>` can now also report affected snapshot if a packfile
|
||||
is corrupted but still present in the repository index.
|
||||
|
||||
https://github.com/restic/restic/pull/21883
|
||||
@@ -0,0 +1,8 @@
|
||||
Enhancement: Reduce memory usage of the in-memory index
|
||||
|
||||
Restic keeps an index of the repository contents in memory during most
|
||||
operations. This index now requires roughly 12% less memory, which is
|
||||
especially noticeable for large repositories with many snapshots, where
|
||||
a lot of index data has to be loaded.
|
||||
|
||||
https://github.com/restic/restic/pull/21937
|
||||
@@ -0,0 +1,9 @@
|
||||
Bugfix: Report an error instead of crashing on a truncated key or config file
|
||||
|
||||
Restic crashed with a slice bounds panic when a repository contained a key
|
||||
file or a config file that was shorter than the encryption overhead, for
|
||||
example after an upload was interrupted. Since every command has to read a
|
||||
key file, the whole repository became unusable until the file was removed.
|
||||
Restic now reports that the file is too short instead.
|
||||
|
||||
https://github.com/restic/restic/pull/21977
|
||||
@@ -0,0 +1,7 @@
|
||||
Bugfix: Redact Swift backend password in debug log output
|
||||
|
||||
Restic could write the OpenStack Swift backend password (OS_PASSWORD or
|
||||
ST_KEY) in clear text to the debug log when debug logging was enabled.
|
||||
The password is now redacted.
|
||||
|
||||
https://github.com/restic/restic/pull/22007
|
||||
+37
-30
@@ -10,6 +10,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -53,22 +54,7 @@ Exit status is 11 if the repository is already locked.
|
||||
Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
PreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
if envVal := os.Getenv("RESTIC_READ_CONCURRENCY"); envVal != "" && !opts.readConcurrencyFlag.Changed {
|
||||
n, err := strconv.ParseUint(envVal, 10, 32)
|
||||
if err != nil {
|
||||
return errors.Fatalf("invalid value for RESTIC_READ_CONCURRENCY %q: %v", envVal, err)
|
||||
}
|
||||
opts.ReadConcurrency = uint(n)
|
||||
}
|
||||
if opts.Host == "" {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
debug.Log("os.Hostname() returned err: %v", err)
|
||||
return nil
|
||||
}
|
||||
opts.Host = hostname
|
||||
}
|
||||
return nil
|
||||
return opts.Finalize()
|
||||
},
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
@@ -143,8 +129,8 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
||||
f.StringArrayVar(&opts.FilesFromRaw, "files-from-raw", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
||||
f.StringVar(&opts.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
|
||||
f.BoolVar(&opts.WithAtime, "with-atime", false, "store the atime for all files and directories")
|
||||
f.BoolVar(&opts.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files")
|
||||
f.BoolVar(&opts.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files")
|
||||
f.BoolVar(&opts.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files (default: $RESTIC_IGNORE_INODE or false)")
|
||||
f.BoolVar(&opts.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files (default: $RESTIC_IGNORE_CTIME or false)")
|
||||
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not upload or write any data, just show what would be done")
|
||||
f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -157,12 +143,35 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
||||
|
||||
opts.readConcurrencyFlag = f.Lookup("read-concurrency")
|
||||
|
||||
// parse read inode and ctime from env, on error the default value will be used
|
||||
opts.IgnoreInode, _ = strconv.ParseBool(os.Getenv("RESTIC_IGNORE_INODE"))
|
||||
opts.IgnoreCtime, _ = strconv.ParseBool(os.Getenv("RESTIC_IGNORE_CTIME"))
|
||||
|
||||
// parse host from env, if not exists or empty the default value will be used
|
||||
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||
opts.Host = host
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *BackupOptions) Finalize() error {
|
||||
if envVal := os.Getenv("RESTIC_READ_CONCURRENCY"); envVal != "" && !opts.readConcurrencyFlag.Changed {
|
||||
n, err := strconv.ParseUint(envVal, 10, 32)
|
||||
if err != nil {
|
||||
return errors.Fatalf("invalid value for RESTIC_READ_CONCURRENCY %q: %v", envVal, err)
|
||||
}
|
||||
opts.ReadConcurrency = uint(n)
|
||||
}
|
||||
if opts.Host == "" {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
debug.Log("os.Hostname() returned err: %v", err)
|
||||
return nil
|
||||
}
|
||||
opts.Host = hostname
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var backupFSTestHook func(fs fs.FS) fs.FS
|
||||
|
||||
// ErrInvalidSourceData is used to report an incomplete backup
|
||||
@@ -173,7 +182,7 @@ var ErrNoSourceData = errors.Fatal("all source directories/files do not exist")
|
||||
|
||||
// filterExisting returns the items that exist and can be accessed. It returns
|
||||
// ErrNoSourceData if none remain, or ErrInvalidSourceData if some were skipped.
|
||||
func filterExisting(items []string, warnf func(msg string, args ...interface{})) (result []string, err error) {
|
||||
func filterExisting(items []string, warnf func(msg string, args ...any)) (result []string, err error) {
|
||||
for _, item := range items {
|
||||
_, err := fs.Lstat(item)
|
||||
if err != nil {
|
||||
@@ -296,10 +305,8 @@ func (opts BackupOptions) Check(gopts global.Options, args []string) error {
|
||||
}
|
||||
|
||||
filesFrom := append(append(opts.FilesFrom, opts.FilesFromVerbatim...), opts.FilesFromRaw...)
|
||||
for _, filename := range filesFrom {
|
||||
if filename == "-" {
|
||||
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
|
||||
}
|
||||
if slices.Contains(filesFrom, "-") {
|
||||
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +331,7 @@ func (opts BackupOptions) Check(gopts global.Options, args []string) error {
|
||||
|
||||
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
||||
// from being saved in a snapshot based on path only
|
||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...interface{})) (fs []archiver.RejectByNameFunc, err error) {
|
||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...any)) (fs []archiver.RejectByNameFunc, err error) {
|
||||
// exclude restic cache
|
||||
if repo.Cache() != nil {
|
||||
f, err := rejectResticCache(repo)
|
||||
@@ -348,7 +355,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, w
|
||||
|
||||
// collectRejectFuncs returns a list of all functions which may reject data
|
||||
// from being saved in a snapshot based on path and file info
|
||||
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...interface{})) (funcs []archiver.RejectFunc, err error) {
|
||||
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...any)) (funcs []archiver.RejectFunc, err error) {
|
||||
// allowed devices
|
||||
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
|
||||
f, err := archiver.RejectByDevice(targets, fs)
|
||||
@@ -396,7 +403,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf fu
|
||||
}
|
||||
|
||||
// collectTargets returns a list of target files/dirs from several sources.
|
||||
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{}), stdin io.ReadCloser) (targets []string, err error) {
|
||||
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...any), stdin io.ReadCloser) (targets []string, err error) {
|
||||
if opts.Stdin || opts.StdinCommand {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -537,8 +544,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
progressReporter := backup.NewProgress(printer,
|
||||
ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
|
||||
progressReporter := backup.NewProgress(printer, gopts.Quiet, gopts.JSON, term.CanUpdateStatus())
|
||||
defer progressReporter.Done()
|
||||
|
||||
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
|
||||
@@ -572,7 +578,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
|
||||
return err
|
||||
}
|
||||
|
||||
var targetFS fs.FS = fs.Local{}
|
||||
targetFS := fs.NewLocal()
|
||||
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
|
||||
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
|
||||
return err
|
||||
@@ -582,7 +588,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
|
||||
_ = progressReporter.Error(item, err)
|
||||
}
|
||||
|
||||
messageHandler := func(msg string, args ...interface{}) {
|
||||
messageHandler := func(msg string, args ...any) {
|
||||
if !gopts.JSON {
|
||||
printer.P(msg, args...)
|
||||
}
|
||||
@@ -663,6 +669,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
|
||||
arch.CompleteItem = progressReporter.CompleteItem
|
||||
arch.StartFile = progressReporter.StartFile
|
||||
arch.CompleteBlob = progressReporter.CompleteBlob
|
||||
arch.ExcludedItem = progressReporter.ExcludedItem
|
||||
|
||||
if opts.IgnoreInode {
|
||||
// --ignore-inode implies --ignore-ctime: on FUSE, the ctime is not
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -13,8 +17,10 @@ import (
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/backup"
|
||||
)
|
||||
|
||||
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) error {
|
||||
@@ -30,6 +36,13 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts
|
||||
})
|
||||
}
|
||||
|
||||
func testRunBackupOutput(t testing.TB, opts BackupOptions, gopts global.Options, target []string) ([]byte, error) {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runBackup(ctx, opts, gopts, gopts.Term, target)
|
||||
})
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) {
|
||||
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
|
||||
rtest.Assert(t, err == nil, "Error while backing up: %v", err)
|
||||
@@ -49,6 +62,9 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
// Use auto compression to ensure coverage in the integration tests
|
||||
// All other tests use fastest compression for faster execution
|
||||
env.gopts.Compression = repository.CompressionAuto
|
||||
testSetupBackupData(t, env)
|
||||
opts := BackupOptions{UseFsSnapshot: useFsSnapshot}
|
||||
|
||||
@@ -139,7 +155,7 @@ func (f *vssDeleteOriginalFS) Lstat(name string) (*fs.ExtendedFileInfo, error) {
|
||||
_, _ = f.FS.Lstat(name)
|
||||
// nuke testdata
|
||||
var err error
|
||||
for i := 0; i < 3; i++ {
|
||||
for range 3 {
|
||||
// The CI sometimes runs into "The process cannot access the file because it is being used by another process" errors
|
||||
// thus try a few times to remove the data
|
||||
err = os.RemoveAll(f.testdata)
|
||||
@@ -615,13 +631,7 @@ func linkEqual(source, dest []string) bool {
|
||||
}
|
||||
|
||||
for i := range source {
|
||||
found := false
|
||||
for j := range dest {
|
||||
if source[i] == dest[j] {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found := slices.Contains(dest, source[i])
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
@@ -723,10 +733,47 @@ func TestBackupSkipIfUnchanged(t *testing.T) {
|
||||
testSetupBackupData(t, env)
|
||||
opts := BackupOptions{SkipIfUnchanged: true}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
for range 3 {
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
||||
testListSnapshots(t, env.gopts, 1)
|
||||
}
|
||||
|
||||
testRunCheck(t, env.gopts)
|
||||
}
|
||||
|
||||
func TestBackupExcludeWithOutput(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
backupOptions := BackupOptions{}
|
||||
backupOptions.Excludes = []string{"*.py"}
|
||||
|
||||
env.gopts.JSON = true
|
||||
env.gopts.Verbosity = 2
|
||||
output, err := testRunBackupOutput(t, backupOptions, env.gopts, []string{filepath.Join(env.testdata, "0", "for_cmd_ls")})
|
||||
rtest.OK(t, err)
|
||||
|
||||
foundExclude := false
|
||||
for line := range bytes.SplitSeq(output, []byte("\n")) {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
type MessageType struct {
|
||||
MessageType string `json:"message_type"` // any
|
||||
}
|
||||
var mType MessageType
|
||||
rtest.OK(t, json.Unmarshal(line, &mType))
|
||||
if mType.MessageType != "excluded_item" {
|
||||
continue
|
||||
}
|
||||
|
||||
var excludeLine backup.VerboseExclude
|
||||
rtest.OK(t, json.Unmarshal(line, &excludeLine))
|
||||
rtest.Assert(t, strings.Contains(excludeLine.Item, ".py"), "expected excluded pathname to be ending in .py, but contains %q",
|
||||
excludeLine.Item)
|
||||
foundExclude = true
|
||||
}
|
||||
rtest.Assert(t, foundExclude, "expected at least one excluded item, but found none")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -57,7 +58,7 @@ func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
func runCache(opts CacheOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if len(args) > 0 {
|
||||
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
|
||||
|
||||
+8
-12
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
|
||||
@@ -48,13 +50,7 @@ func validateCatArgs(args []string) error {
|
||||
return errors.Fatal("type not specified")
|
||||
}
|
||||
|
||||
validType := false
|
||||
for _, v := range catAllowedCmds {
|
||||
if v == args[0] {
|
||||
validType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
validType := slices.Contains(catAllowedCmds, args[0])
|
||||
if !validType {
|
||||
return errors.Fatalf("invalid type %q, must be one of [%s]", args[0], strings.Join(catAllowedCmds, "|"))
|
||||
}
|
||||
@@ -67,7 +63,7 @@ func validateCatArgs(args []string) error {
|
||||
}
|
||||
|
||||
func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
if err := validateCatArgs(args); err != nil {
|
||||
return err
|
||||
@@ -141,7 +137,7 @@ func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Te
|
||||
printer.S(string(buf))
|
||||
return nil
|
||||
case "lock":
|
||||
lock, err := restic.LoadLock(ctx, repo, id)
|
||||
lock, err := repository.LoadLock(ctx, repo, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,11 +172,11 @@ func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Te
|
||||
}
|
||||
|
||||
for _, t := range []restic.BlobType{restic.DataBlob, restic.TreeBlob} {
|
||||
if _, ok := repo.LookupBlobSize(t, id); !ok {
|
||||
if _, ok := repo.LookupBlobSize(restic.BlobHandle{Type: t, ID: id}); !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
buf, err := repo.LoadBlob(ctx, t, id, nil)
|
||||
buf, err := repo.LoadBlob(ctx, restic.BlobHandle{Type: t, ID: id}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -207,7 +203,7 @@ func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Te
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := repo.LoadBlob(ctx, restic.TreeBlob, *sn.Tree, nil)
|
||||
buf, err := repo.LoadBlob(ctx, restic.BlobHandle{Type: restic.TreeBlob, ID: *sn.Tree}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+33
-26
@@ -179,7 +179,7 @@ func parsePercentage(s string) (float64, error) {
|
||||
// - if the user explicitly requested --no-cache, we don't use any cache
|
||||
// - if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check
|
||||
// - by default, we use a cache in a temporary directory that is deleted after the check
|
||||
func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progress.Printer) (cleanup func()) {
|
||||
func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer restic.Printer) (cleanup func()) {
|
||||
cleanup = func() {}
|
||||
if opts.WithCache {
|
||||
// use the default cache, no setup needed
|
||||
@@ -229,9 +229,9 @@ func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progres
|
||||
func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args []string, term ui.Terminal) (checkSummary, error) {
|
||||
summary := checkSummary{MessageType: "summary"}
|
||||
|
||||
var printer progress.Printer
|
||||
var printer restic.Printer
|
||||
if !gopts.JSON {
|
||||
printer = ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer = progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
} else {
|
||||
printer = newJSONErrorPrinter(term)
|
||||
}
|
||||
@@ -307,13 +307,13 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
||||
go chkr.Packs(ctx, errChan)
|
||||
|
||||
for err := range errChan {
|
||||
var packErr *repository.PackError
|
||||
var packErr *repository.ErrPackMetadata
|
||||
if errors.As(err, &packErr) {
|
||||
if packErr.Orphaned {
|
||||
orphanedPacks++
|
||||
printer.V("%v\n", err)
|
||||
} else {
|
||||
if packErr.Truncated {
|
||||
if packErr.Truncated || packErr.Missing {
|
||||
salvagePacks.Insert(packErr.ID)
|
||||
}
|
||||
errorsFound = true
|
||||
@@ -339,25 +339,28 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
||||
|
||||
printer.P("check snapshots, trees and blobs\n")
|
||||
errChan = make(chan error)
|
||||
var brokenSnapshots []string
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
bar := printer.NewCounter("snapshots")
|
||||
defer bar.Done()
|
||||
chkr.Structure(ctx, bar, errChan)
|
||||
}()
|
||||
})
|
||||
|
||||
for err := range errChan {
|
||||
errorsFound = true
|
||||
if e, ok := err.(*checker.TreeError); ok {
|
||||
switch e := err.(type) {
|
||||
case *checker.TreeError:
|
||||
printer.E("error for tree %v:\n", e.ID.Str())
|
||||
for _, treeErr := range e.Errors {
|
||||
summary.NumErrors++
|
||||
printer.E(" %v\n", treeErr)
|
||||
}
|
||||
} else {
|
||||
case *checker.SnapshotError:
|
||||
printer.E("snapshot error %v: %v", e.ID, e.Message)
|
||||
brokenSnapshots = append(brokenSnapshots, e.ID)
|
||||
default:
|
||||
summary.NumErrors++
|
||||
printer.E("error: %v\n", err)
|
||||
}
|
||||
@@ -389,10 +392,9 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
||||
}
|
||||
|
||||
if readDataFilter != nil {
|
||||
p := printer.NewCounter("packs")
|
||||
errChan := make(chan error)
|
||||
|
||||
go chkr.ReadPacks(ctx, readDataFilter, p, errChan)
|
||||
go chkr.ReadPacks(ctx, readDataFilter, printer, errChan)
|
||||
|
||||
for err := range errChan {
|
||||
errorsFound = true
|
||||
@@ -402,7 +404,6 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
||||
salvagePacks.Insert(err.PackID)
|
||||
}
|
||||
}
|
||||
p.Done()
|
||||
}
|
||||
|
||||
if len(salvagePacks) > 0 {
|
||||
@@ -414,12 +415,18 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
||||
printer.E("Damaged pack files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!\n")
|
||||
}
|
||||
|
||||
if len(brokenSnapshots) > 0 {
|
||||
printer.E("\nThe repository contains damaged snapshot files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.\n\n")
|
||||
printer.E("restic repair snapshots --forget %s\n\n", strings.Join(brokenSnapshots, " "))
|
||||
printer.E("Damaged snapshot files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!\n")
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return summary, ctx.Err()
|
||||
}
|
||||
|
||||
if errorsFound {
|
||||
if len(salvagePacks) == 0 {
|
||||
if len(salvagePacks) == 0 && len(brokenSnapshots) == 0 {
|
||||
printer.E("\nThe repository is damaged and must be repaired. Please follow the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html .\n\n")
|
||||
}
|
||||
return summary, errors.Fatal("repository contains errors")
|
||||
@@ -428,7 +435,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func buildPacksFilter(opts CheckOptions, printer progress.Printer,
|
||||
func buildPacksFilter(opts CheckOptions, printer restic.Printer,
|
||||
filteredStatus bool) (func(packs map[restic.ID]int64) map[restic.ID]int64, error) {
|
||||
typeData := ""
|
||||
if filteredStatus {
|
||||
@@ -553,23 +560,23 @@ func newJSONErrorPrinter(term ui.Terminal) *jsonErrorPrinter {
|
||||
}
|
||||
}
|
||||
|
||||
func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter {
|
||||
return nil
|
||||
func (*jsonErrorPrinter) NewCounter(_ string) restic.Counter {
|
||||
return restic.NoopCounter
|
||||
}
|
||||
|
||||
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) *progress.Counter {
|
||||
return nil
|
||||
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) restic.Counter {
|
||||
return restic.NoopCounter
|
||||
}
|
||||
|
||||
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
||||
func (p *jsonErrorPrinter) E(msg string, args ...any) {
|
||||
status := checkError{
|
||||
MessageType: "error",
|
||||
Message: fmt.Sprintf(msg, args...),
|
||||
}
|
||||
p.term.Error(ui.ToJSONString(status))
|
||||
}
|
||||
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) S(_ string, _ ...any) {}
|
||||
func (*jsonErrorPrinter) P(_ string, _ ...any) {}
|
||||
func (*jsonErrorPrinter) PT(_ string, _ ...any) {}
|
||||
func (*jsonErrorPrinter) V(_ string, _ ...any) {}
|
||||
func (*jsonErrorPrinter) VV(_ string, _ ...any) {}
|
||||
|
||||
@@ -11,35 +11,36 @@ import (
|
||||
|
||||
func testRunCheck(t testing.TB, gopts global.Options) {
|
||||
t.Helper()
|
||||
output, err := testRunCheckOutput(t, gopts, true)
|
||||
stdout, stderr, err := testRunCheckOutput(t, gopts, true)
|
||||
if err != nil {
|
||||
t.Error(output)
|
||||
t.Error(stdout)
|
||||
t.Error(stderr)
|
||||
t.Fatalf("unexpected error: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testRunCheckMustFail(t testing.TB, gopts global.Options) {
|
||||
t.Helper()
|
||||
_, err := testRunCheckOutput(t, gopts, false)
|
||||
_, _, err := testRunCheckOutput(t, gopts, false)
|
||||
rtest.Assert(t, err != nil, "expected non nil error after check of damaged repository")
|
||||
}
|
||||
|
||||
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, error) {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, string, error) {
|
||||
stdout, stderr, err := withCaptureStdoutStderr(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
opts := CheckOptions{
|
||||
ReadData: true,
|
||||
CheckUnused: checkUnused,
|
||||
}
|
||||
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.Term)
|
||||
_, err := runCheck(ctx, opts, gopts, nil, gopts.Term)
|
||||
return err
|
||||
})
|
||||
return buf.String(), err
|
||||
return stdout.String(), stderr.String(), err
|
||||
}
|
||||
|
||||
func testRunCheckOutputWithOpts(t testing.TB, gopts global.Options, opts CheckOptions, args []string) (string, error) {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
gopts.Verbosity = 2
|
||||
_, err := runCheck(context.TODO(), opts, gopts, args, gopts.Term)
|
||||
_, err := runCheck(ctx, opts, gopts, args, gopts.Term)
|
||||
return err
|
||||
})
|
||||
return buf.String(), err
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func TestParsePercentage(t *testing.T) {
|
||||
@@ -204,7 +203,7 @@ func TestPrepareCheckCache(t *testing.T) {
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
gopts := global.Options{CacheDir: tmpDirBase}
|
||||
cleanup := prepareCheckCache(testCase.opts, &gopts, &progress.NoopPrinter{})
|
||||
cleanup := prepareCheckCache(testCase.opts, &gopts, restic.NewNoopPrinter())
|
||||
files, err := os.ReadDir(tmpDirBase)
|
||||
rtest.OK(t, err)
|
||||
|
||||
@@ -234,7 +233,7 @@ func TestPrepareCheckCache(t *testing.T) {
|
||||
|
||||
func TestPrepareDefaultCheckCache(t *testing.T) {
|
||||
gopts := global.Options{CacheDir: ""}
|
||||
cleanup := prepareCheckCache(CheckOptions{}, &gopts, &progress.NoopPrinter{})
|
||||
cleanup := prepareCheckCache(CheckOptions{}, &gopts, restic.NewNoopPrinter())
|
||||
_, err := os.ReadDir(gopts.CacheDir)
|
||||
rtest.OK(t, err)
|
||||
|
||||
|
||||
+45
-26
@@ -72,14 +72,22 @@ func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
|
||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||
}
|
||||
|
||||
var errSentinelEndIteration = errors.New("end iteration")
|
||||
|
||||
// collectAllSnapshots: select all snapshot trees to be copied
|
||||
func collectAllSnapshots(ctx context.Context, opts CopyOptions,
|
||||
srcSnapshotLister restic.Lister, srcRepo restic.Repository,
|
||||
dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer progress.Printer,
|
||||
) iter.Seq[*data.Snapshot] {
|
||||
return func(yield func(*data.Snapshot) bool) {
|
||||
for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args, printer) {
|
||||
dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer restic.Printer,
|
||||
) iter.Seq2[*data.Snapshot, error] {
|
||||
return func(yield func(*data.Snapshot, error) bool) {
|
||||
err := opts.SnapshotFilter.FindAll(ctx, srcSnapshotLister, srcRepo, args, func(_ string, sn *data.Snapshot, err error) error {
|
||||
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
|
||||
if err != nil {
|
||||
if !yield(nil, err) {
|
||||
return errSentinelEndIteration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
srcOriginal := *sn.ID()
|
||||
if sn.Original != nil {
|
||||
srcOriginal = *sn.Original
|
||||
@@ -95,18 +103,22 @@ func collectAllSnapshots(ctx context.Context, opts CopyOptions,
|
||||
}
|
||||
}
|
||||
if isCopy {
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !yield(sn) {
|
||||
return
|
||||
if !yield(sn, nil) {
|
||||
return errSentinelEndIteration
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil && !errors.Is(err, errSentinelEndIteration) {
|
||||
yield(nil, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runCopy(ctx context.Context, opts CopyOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
secondaryGopts, isFromRepo, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "destination")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -148,15 +160,19 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts global.Options, args [
|
||||
}
|
||||
|
||||
dstSnapshotByOriginal := make(map[restic.ID][]*data.Snapshot)
|
||||
for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil, printer) {
|
||||
err = opts.SnapshotFilter.FindAll(ctx, dstSnapshotLister, dstRepo, nil, func(_ string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sn.Original != nil && !sn.Original.IsNull() {
|
||||
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
||||
}
|
||||
// also consider identical snapshot copies
|
||||
dstSnapshotByOriginal[*sn.ID()] = append(dstSnapshotByOriginal[*sn.ID()], sn)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedSnapshots := collectAllSnapshots(ctx, opts, srcSnapshotLister, srcRepo, dstSnapshotByOriginal, args, printer)
|
||||
@@ -189,8 +205,8 @@ func similarSnapshots(sna *data.Snapshot, snb *data.Snapshot) bool {
|
||||
|
||||
// copyTreeBatched copies multiple snapshots in one go. Snapshots are written after
|
||||
// data equivalent to at least 10 packfiles was written.
|
||||
func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
||||
selectedSnapshots iter.Seq[*data.Snapshot], printer progress.Printer) error {
|
||||
func copyTreeBatched(ctx context.Context, srcRepo *repository.Repository, dstRepo restic.Repository,
|
||||
selectedSnapshots iter.Seq2[*data.Snapshot, error], printer restic.Printer) error {
|
||||
|
||||
// remember already processed trees across all snapshots
|
||||
visitedTrees := srcRepo.NewAssociatedBlobSet()
|
||||
@@ -199,7 +215,7 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
|
||||
minDuration := 1 * time.Minute
|
||||
|
||||
// use pull-based iterator to allow iteration in multiple steps
|
||||
next, stop := iter.Pull(selectedSnapshots)
|
||||
next, stop := iter.Pull2(selectedSnapshots)
|
||||
defer stop()
|
||||
|
||||
for {
|
||||
@@ -210,7 +226,10 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
|
||||
// call WithBlobUploader() once and then loop over all selectedSnapshots
|
||||
err := dstRepo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||
for batchSize < targetSize || time.Since(startTime) < minDuration {
|
||||
sn, ok := next()
|
||||
sn, err, ok := next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
@@ -254,8 +273,8 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
||||
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer progress.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
|
||||
func copyTree(ctx context.Context, srcRepo *repository.Repository, dstRepo restic.Repository,
|
||||
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer restic.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
|
||||
|
||||
copyBlobs := srcRepo.NewAssociatedBlobSet()
|
||||
packList := restic.NewIDSet()
|
||||
@@ -264,16 +283,16 @@ func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Rep
|
||||
enqueue := func(h restic.BlobHandle) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
|
||||
pb := srcRepo.LookupBlob(h.Type, h.ID)
|
||||
if _, ok := dstRepo.LookupBlobSize(h); !ok {
|
||||
pb := srcRepo.LookupBlob(h)
|
||||
copyBlobs.Insert(h)
|
||||
for _, p := range pb {
|
||||
packList.Insert(p.PackID)
|
||||
packList.Insert(p.PackID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := data.StreamTrees(ctx, srcRepo, restic.IDs{rootTreeID}, nil, func(treeID restic.ID) bool {
|
||||
err := data.StreamTrees(ctx, srcRepo, restic.IDs{rootTreeID}, restic.NoopCounter, func(treeID restic.ID) bool {
|
||||
handle := restic.BlobHandle{ID: treeID, Type: restic.TreeBlob}
|
||||
visited := visitedTrees.Has(handle)
|
||||
visitedTrees.Insert(handle)
|
||||
@@ -312,14 +331,14 @@ func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Rep
|
||||
}
|
||||
|
||||
// copyStats: print statistics for the blobs to be copied
|
||||
func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, packList restic.IDSet, printer progress.Printer) uint64 {
|
||||
func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, packList restic.IDSet, printer restic.Printer) uint64 {
|
||||
// count and size
|
||||
countBlobs := 0
|
||||
sizeBlobs := uint64(0)
|
||||
for blob := range copyBlobs.Keys() {
|
||||
for _, blob := range srcRepo.LookupBlob(blob.Type, blob.ID) {
|
||||
for _, pb := range srcRepo.LookupBlob(blob) {
|
||||
countBlobs++
|
||||
sizeBlobs += uint64(blob.Length)
|
||||
sizeBlobs += uint64(pb.CiphertextLength())
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -329,7 +348,7 @@ func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, pa
|
||||
return sizeBlobs
|
||||
}
|
||||
|
||||
func copySaveSnapshot(ctx context.Context, sn *data.Snapshot, dstRepo restic.Repository, printer progress.Printer) error {
|
||||
func copySaveSnapshot(ctx context.Context, sn *data.Snapshot, dstRepo restic.Repository, printer restic.Printer) error {
|
||||
sn.Parent = nil // Parent does not have relevance in the new repo.
|
||||
// Use Original as a persistent snapshot ID
|
||||
if sn.Original == nil {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func testRunCopy(t testing.TB, srcGopts global.Options, dstGopts global.Options) {
|
||||
@@ -97,23 +97,23 @@ func TestCopy(t *testing.T) {
|
||||
|
||||
func testPackAndBlobCounts(t testing.TB, gopts global.Options) (countTreePacks int, countDataPacks int, countBlobs int) {
|
||||
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
rtest.OK(t, repo.List(context.TODO(), restic.PackFile, func(id restic.ID, size int64) error {
|
||||
blobs, _, err := repo.ListPack(context.TODO(), id, size)
|
||||
handles, err := repo.ListPackHandles(context.TODO(), id, size)
|
||||
rtest.OK(t, err)
|
||||
rtest.Assert(t, len(blobs) > 0, "a packfile should contain at least one blob")
|
||||
rtest.Assert(t, len(handles) > 0, "a packfile should contain at least one blob")
|
||||
|
||||
switch blobs[0].Type {
|
||||
switch handles[0].Type {
|
||||
case restic.TreeBlob:
|
||||
countTreePacks++
|
||||
case restic.DataBlob:
|
||||
countDataPacks++
|
||||
}
|
||||
countBlobs += len(blobs)
|
||||
countBlobs += len(handles)
|
||||
return nil
|
||||
}))
|
||||
return nil
|
||||
|
||||
+12
-366
@@ -4,28 +4,17 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/restic/restic/internal/crypto"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/repository/index"
|
||||
"github.com/restic/restic/internal/repository/pack"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
@@ -128,63 +117,8 @@ func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io
|
||||
})
|
||||
}
|
||||
|
||||
// Pack is the struct used in printPacks.
|
||||
type Pack struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
Blobs []Blob `json:"blobs"`
|
||||
}
|
||||
|
||||
// Blob is the struct used in printPacks.
|
||||
type Blob struct {
|
||||
Type restic.BlobType `json:"type"`
|
||||
Length uint `json:"length"`
|
||||
ID restic.ID `json:"id"`
|
||||
Offset uint `json:"offset"`
|
||||
}
|
||||
|
||||
func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer, printer progress.Printer) error {
|
||||
|
||||
var m sync.Mutex
|
||||
return restic.ParallelList(ctx, repo, restic.PackFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error {
|
||||
blobs, _, err := repo.ListPack(ctx, id, size)
|
||||
if err != nil {
|
||||
printer.E("error for pack %v: %v", id.Str(), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
p := Pack{
|
||||
Name: id.String(),
|
||||
Blobs: make([]Blob, len(blobs)),
|
||||
}
|
||||
for i, blob := range blobs {
|
||||
p.Blobs[i] = Blob{
|
||||
Type: blob.Type,
|
||||
Length: blob.Length,
|
||||
ID: blob.ID,
|
||||
Offset: blob.Offset,
|
||||
}
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return prettyPrintJSON(wr, p)
|
||||
})
|
||||
}
|
||||
|
||||
func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer, printer progress.Printer) error {
|
||||
return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error {
|
||||
printer.S("index_id: %v", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return idx.Dump(wr)
|
||||
})
|
||||
}
|
||||
|
||||
func runDebugDump(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.Fatal("type not specified")
|
||||
@@ -200,11 +134,11 @@ func runDebugDump(ctx context.Context, gopts global.Options, args []string, term
|
||||
|
||||
switch tpe {
|
||||
case "indexes":
|
||||
return dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
return repository.DumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
case "snapshots":
|
||||
return debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
||||
case "packs":
|
||||
return printPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
return repository.DumpPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
case "all":
|
||||
printer.S("snapshots:")
|
||||
err := debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
||||
@@ -213,7 +147,7 @@ func runDebugDump(ctx context.Context, gopts global.Options, args []string, term
|
||||
}
|
||||
|
||||
printer.S("indexes:")
|
||||
err = dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
err = repository.DumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -224,227 +158,8 @@ func runDebugDump(ctx context.Context, gopts global.Options, args []string, term
|
||||
}
|
||||
}
|
||||
|
||||
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer progress.Printer) []byte {
|
||||
if bytewise {
|
||||
printer.S(" trying to repair blob by finding a broken byte")
|
||||
} else {
|
||||
printer.S(" trying to repair blob with single bit flip")
|
||||
}
|
||||
|
||||
ch := make(chan int)
|
||||
var wg errgroup.Group
|
||||
done := make(chan struct{})
|
||||
var fixed []byte
|
||||
var found bool
|
||||
|
||||
workers := runtime.GOMAXPROCS(0)
|
||||
printer.S(" spinning up %d worker functions", runtime.GOMAXPROCS(0))
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Go(func() error {
|
||||
// make a local copy of the buffer
|
||||
buf := make([]byte, len(input))
|
||||
copy(buf, input)
|
||||
|
||||
testFlip := func(idx int, pattern byte) bool {
|
||||
// flip bits
|
||||
buf[idx] ^= pattern
|
||||
|
||||
nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
|
||||
plaintext, err := key.Open(plaintext[:0], nonce, plaintext, nil)
|
||||
if err == nil {
|
||||
printer.S("")
|
||||
printer.S(" blob could be repaired by XORing byte %v with 0x%02x", idx, pattern)
|
||||
printer.S(" hash is %v", restic.Hash(plaintext))
|
||||
close(done)
|
||||
found = true
|
||||
fixed = plaintext
|
||||
return true
|
||||
}
|
||||
|
||||
// flip bits back
|
||||
buf[idx] ^= pattern
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range ch {
|
||||
if bytewise {
|
||||
for j := 0; j < 255; j++ {
|
||||
if testFlip(i, byte(j)) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for j := 0; j < 7; j++ {
|
||||
// flip each bit once
|
||||
if testFlip(i, (1 << uint(j))) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
wg.Go(func() error {
|
||||
defer close(ch)
|
||||
|
||||
start := time.Now()
|
||||
info := time.Now()
|
||||
for i := range input {
|
||||
select {
|
||||
case ch <- i:
|
||||
case <-done:
|
||||
printer.S(" done after %v", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
|
||||
if time.Since(info) > time.Second {
|
||||
secs := time.Since(start).Seconds()
|
||||
gps := float64(i) / secs
|
||||
remaining := len(input) - i
|
||||
eta := time.Duration(float64(remaining)/gps) * time.Second
|
||||
|
||||
printer.S("\r%d byte of %d done (%.2f%%), %.0f byte per second, ETA %v",
|
||||
i, len(input), float32(i)/float32(len(input))*100, gps, eta)
|
||||
info = time.Now()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
err := wg.Wait()
|
||||
if err != nil {
|
||||
panic("all go routines can only return nil")
|
||||
}
|
||||
|
||||
if !found {
|
||||
printer.S("\n blob could not be repaired")
|
||||
}
|
||||
return fixed
|
||||
}
|
||||
|
||||
func decryptUnsigned(k *crypto.Key, buf []byte) []byte {
|
||||
// strip signature at the end
|
||||
l := len(buf)
|
||||
nonce, ct := buf[:16], buf[16:l-16]
|
||||
out := make([]byte, len(ct))
|
||||
|
||||
c, err := aes.NewCipher(k.EncryptionKey[:])
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unable to create cipher: %v", err))
|
||||
}
|
||||
e := cipher.NewCTR(c, nonce)
|
||||
e.XORKeyStream(out, ct)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, packID restic.ID, list restic.Blobs, printer progress.Printer) error {
|
||||
dec, err := zstd.NewReader(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pack, err := repo.LoadRaw(ctx, restic.PackFile, packID)
|
||||
// allow processing broken pack files
|
||||
if pack == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||
for _, blob := range list {
|
||||
printer.S(" loading blob %v at %v (length %v)", blob.ID, blob.Offset, blob.Length)
|
||||
if int(blob.Offset+blob.Length) > len(pack) {
|
||||
printer.E("skipping truncated blob")
|
||||
continue
|
||||
}
|
||||
buf := pack[blob.Offset : blob.Offset+blob.Length]
|
||||
key := repo.Key()
|
||||
|
||||
nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
|
||||
plaintext, err = key.Open(plaintext[:0], nonce, plaintext, nil)
|
||||
outputPrefix := ""
|
||||
filePrefix := ""
|
||||
if err != nil {
|
||||
printer.E("error decrypting blob: %v", err)
|
||||
if opts.TryRepair || opts.RepairByte {
|
||||
plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte, printer)
|
||||
}
|
||||
if plaintext != nil {
|
||||
outputPrefix = "repaired "
|
||||
filePrefix = "repaired-"
|
||||
} else {
|
||||
plaintext = decryptUnsigned(key, buf)
|
||||
err = storePlainBlob(blob.ID, "damaged-", plaintext, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if blob.IsCompressed() {
|
||||
decompressed, err := dec.DecodeAll(plaintext, nil)
|
||||
if err != nil {
|
||||
printer.S(" failed to decompress blob %v", blob.ID)
|
||||
}
|
||||
if decompressed != nil {
|
||||
plaintext = decompressed
|
||||
}
|
||||
}
|
||||
|
||||
id := restic.Hash(plaintext)
|
||||
var prefix string
|
||||
if !id.Equal(blob.ID) {
|
||||
printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID does not match, wanted %v", outputPrefix, len(plaintext), id, blob.ID)
|
||||
prefix = "wrong-hash-"
|
||||
} else {
|
||||
printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID matches", outputPrefix, len(plaintext), id)
|
||||
prefix = "correct-"
|
||||
}
|
||||
if opts.ExtractPack {
|
||||
err = storePlainBlob(id, filePrefix+prefix, plaintext, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if opts.ReuploadBlobs {
|
||||
_, _, _, err := uploader.SaveBlob(ctx, blob.Type, plaintext, id, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printer.S(" uploaded %v %v", blob.Type, id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func storePlainBlob(id restic.ID, prefix string, plain []byte, printer progress.Printer) error {
|
||||
filename := fmt.Sprintf("%s%s.bin", prefix, id)
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(plain)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.S("decrypt of blob %v stored at %v", id, filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamineOptions, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if opts.ExtractPack && gopts.NoLock {
|
||||
return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive")
|
||||
@@ -478,8 +193,14 @@ func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamin
|
||||
return err
|
||||
}
|
||||
|
||||
examineOpts := repository.ExaminePackOptions{
|
||||
TryRepair: opts.TryRepair,
|
||||
RepairByte: opts.RepairByte,
|
||||
ExtractPack: opts.ExtractPack,
|
||||
ReuploadBlobs: opts.ReuploadBlobs,
|
||||
}
|
||||
for _, id := range ids {
|
||||
err := examinePack(ctx, opts, repo, id, printer)
|
||||
err := repository.ExaminePack(ctx, repo, id, examineOpts, printer)
|
||||
if err != nil {
|
||||
printer.E("error: %v", err)
|
||||
}
|
||||
@@ -489,78 +210,3 @@ func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamin
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID, printer progress.Printer) error {
|
||||
printer.S("examine %v", id)
|
||||
|
||||
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
|
||||
// also process damaged pack files
|
||||
if buf == nil {
|
||||
return err
|
||||
}
|
||||
printer.S(" file size is %v", len(buf))
|
||||
gotID := restic.Hash(buf)
|
||||
if !id.Equal(gotID) {
|
||||
printer.S(" wanted hash %v, got %v", id, gotID)
|
||||
} else {
|
||||
printer.S(" hash for file content matches")
|
||||
}
|
||||
|
||||
printer.S(" ========================================")
|
||||
printer.S(" looking for info in the indexes")
|
||||
|
||||
blobsLoaded := false
|
||||
// examine all data the indexes have for the pack file
|
||||
for b := range repo.ListPacksFromIndex(ctx, restic.NewIDSet(id)) {
|
||||
blobs := b.Blobs
|
||||
if len(blobs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
checkPackSize(blobs, len(buf), printer)
|
||||
|
||||
err = loadBlobs(ctx, opts, repo, id, blobs, printer)
|
||||
if err != nil {
|
||||
printer.E("error: %v", err)
|
||||
} else {
|
||||
blobsLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
printer.S(" ========================================")
|
||||
printer.S(" inspect the pack itself")
|
||||
|
||||
blobs, _, err := repo.ListPack(ctx, id, int64(len(buf)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("pack %v: %v", id.Str(), err)
|
||||
}
|
||||
checkPackSize(blobs, len(buf), printer)
|
||||
|
||||
if !blobsLoaded {
|
||||
return loadBlobs(ctx, opts, repo, id, blobs, printer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPackSize(blobs restic.Blobs, fileSize int, printer progress.Printer) {
|
||||
// track current size and offset
|
||||
var size, offset uint64
|
||||
|
||||
blobs.Sort()
|
||||
|
||||
for _, pb := range blobs {
|
||||
printer.S(" %v blob %v, offset %-6d, raw length %-6d", pb.Type, pb.ID, pb.Offset, pb.Length)
|
||||
if offset != uint64(pb.Offset) {
|
||||
printer.S(" hole in file, want offset %v, got %v", offset, pb.Offset)
|
||||
}
|
||||
offset = uint64(pb.Offset + pb.Length)
|
||||
size += uint64(pb.Length)
|
||||
}
|
||||
size += uint64(pack.CalculateHeaderSize(blobs))
|
||||
|
||||
if uint64(fileSize) != size {
|
||||
printer.S(" file sizes do not match: computed %v, file size is %v", size, fileSize)
|
||||
} else {
|
||||
printer.S(" file sizes match")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -83,7 +84,7 @@ type Comparer struct {
|
||||
repo restic.BlobLoader
|
||||
opts DiffOptions
|
||||
printChange func(change *Change)
|
||||
printError func(string, ...interface{})
|
||||
printError func(string, ...any)
|
||||
}
|
||||
|
||||
type Change struct {
|
||||
@@ -157,7 +158,7 @@ type DiffStatsContainer struct {
|
||||
}
|
||||
|
||||
// updateBlobs updates the blob counters in the stats struct.
|
||||
func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *DiffStat, printError func(string, ...interface{})) {
|
||||
func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *DiffStat, printError func(string, ...any)) {
|
||||
for h := range blobs.Keys() {
|
||||
switch h.Type {
|
||||
case restic.DataBlob:
|
||||
@@ -166,7 +167,7 @@ func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *Diff
|
||||
stats.TreeBlobs++
|
||||
}
|
||||
|
||||
size, found := repo.LookupBlobSize(h.Type, h.ID)
|
||||
size, found := repo.LookupBlobSize(h)
|
||||
if !found {
|
||||
printError("unable to find blob size for %v", h)
|
||||
continue
|
||||
@@ -358,7 +359,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts global.Options, args [
|
||||
return errors.Fatalf("specify two snapshot IDs")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -136,7 +137,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts global.Options, args [
|
||||
return errors.Fatal("no file and no snapshot ID specified")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
switch opts.Archive {
|
||||
case "tar", "zip":
|
||||
|
||||
+54
-51
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
)
|
||||
|
||||
@@ -135,9 +136,9 @@ type statefulOutput struct {
|
||||
oldsn *data.Snapshot
|
||||
hits int
|
||||
printer interface {
|
||||
S(string, ...interface{})
|
||||
P(string, ...interface{})
|
||||
E(string, ...interface{})
|
||||
S(string, ...any)
|
||||
P(string, ...any)
|
||||
E(string, ...any)
|
||||
}
|
||||
stdout io.Writer
|
||||
}
|
||||
@@ -213,7 +214,7 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *
|
||||
Path string `json:"path"`
|
||||
ParentTree string `json:"parent_tree,omitempty"`
|
||||
SnapshotID string `json:"snapshot"`
|
||||
Time time.Time `json:"time,omitempty"`
|
||||
Time time.Time `json:"time"`
|
||||
}{
|
||||
ObjectType: kind,
|
||||
ID: id,
|
||||
@@ -280,9 +281,9 @@ type Finder struct {
|
||||
treeIDs map[string]struct{}
|
||||
itemsFound int
|
||||
printer interface {
|
||||
S(string, ...interface{})
|
||||
P(string, ...interface{})
|
||||
E(string, ...interface{})
|
||||
S(string, ...any)
|
||||
P(string, ...any)
|
||||
E(string, ...any)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +378,7 @@ func (f *Finder) findTree(treeID restic.ID, nodepath string) error {
|
||||
f.itemsFound++
|
||||
// Terminate if we have found all trees (and we are not
|
||||
// looking for blobs)
|
||||
if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil {
|
||||
if f.itemsFound >= len(f.treeIDs) && len(f.blobIDs) == 0 {
|
||||
// Return an error to terminate the Walk
|
||||
return errFindDone
|
||||
}
|
||||
@@ -412,13 +413,13 @@ func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.Type == "dir" && f.treeIDs != nil {
|
||||
if node.Type == "dir" && len(f.treeIDs) > 0 {
|
||||
if err := f.findTree(*node.Subtree, nodepath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if node.Type == data.NodeTypeFile && f.blobIDs != nil {
|
||||
if node.Type == data.NodeTypeFile && len(f.blobIDs) > 0 {
|
||||
for _, id := range node.Content {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
@@ -444,6 +445,17 @@ func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
|
||||
|
||||
var errAllPacksFound = errors.New("all packs found")
|
||||
|
||||
func (f *Finder) addBlobHandle(h restic.BlobHandle) {
|
||||
switch h.Type {
|
||||
case restic.DataBlob:
|
||||
f.blobIDs[h.ID.String()] = struct{}{}
|
||||
case restic.TreeBlob:
|
||||
f.treeIDs[h.ID.String()] = struct{}{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type %v in blob list", h.Type.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// packsToBlobs converts the list of pack IDs to a list of blob IDs that
|
||||
// belong to those packs.
|
||||
func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
||||
@@ -467,37 +479,30 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
||||
return nil
|
||||
}
|
||||
delete(packIDs, id.Str())
|
||||
} else {
|
||||
// forget found id
|
||||
delete(packIDs, idStr)
|
||||
packIDs[idStr] = struct{}{}
|
||||
}
|
||||
debug.Log("Found pack %s", idStr)
|
||||
blobs, _, err := f.repo.ListPack(ctx, id, size)
|
||||
handles, err := f.repo.ListPackHandles(ctx, id, size)
|
||||
if err != nil {
|
||||
return err
|
||||
// ignore error to allow fallback to index
|
||||
return nil
|
||||
}
|
||||
for _, b := range blobs {
|
||||
switch b.Type {
|
||||
case restic.DataBlob:
|
||||
f.blobIDs[b.ID.String()] = struct{}{}
|
||||
case restic.TreeBlob:
|
||||
f.treeIDs[b.ID.String()] = struct{}{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type %v in blob list", b.Type.String()))
|
||||
}
|
||||
for _, h := range handles {
|
||||
f.addBlobHandle(h)
|
||||
}
|
||||
// forget successfully processed pack
|
||||
delete(packIDs, idStr)
|
||||
// Stop searching when all packs have been found
|
||||
if len(packIDs) == 0 {
|
||||
return errAllPacksFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil && err != errAllPacksFound {
|
||||
if err != nil && !errors.Is(err, errAllPacksFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != errAllPacksFound {
|
||||
if len(packIDs) > 0 {
|
||||
// try to resolve unknown pack ids from the index
|
||||
packIDs, err = f.indexPacksToBlobs(ctx, packIDs)
|
||||
if err != nil {
|
||||
@@ -515,7 +520,7 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
||||
return errors.Fatalf("unable to find pack(s): %v", list)
|
||||
}
|
||||
|
||||
debug.Log("%d blobs found", len(f.blobIDs))
|
||||
debug.Log("%d blobs %v trees found", len(f.blobIDs), len(f.treeIDs))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -525,22 +530,23 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
|
||||
|
||||
// remember which packs were found in the index
|
||||
indexPackIDs := make(map[string]struct{})
|
||||
err := f.repo.ListBlobs(wctx, func(pb restic.PackedBlob) {
|
||||
idStr := pb.PackID.String()
|
||||
err := f.repo.ListBlobs(wctx, func(pb restic.PackBlob) {
|
||||
packID := pb.PackID()
|
||||
idStr := packID.String()
|
||||
// keep entry in packIDs as Each() returns individual index entries
|
||||
matchingID := false
|
||||
if _, ok := packIDs[idStr]; ok {
|
||||
matchingID = true
|
||||
} else {
|
||||
if _, ok := packIDs[pb.PackID.Str()]; ok {
|
||||
if _, ok := packIDs[packID.Str()]; ok {
|
||||
// expand id
|
||||
delete(packIDs, pb.PackID.Str())
|
||||
delete(packIDs, packID.Str())
|
||||
packIDs[idStr] = struct{}{}
|
||||
matchingID = true
|
||||
}
|
||||
}
|
||||
if matchingID {
|
||||
f.blobIDs[pb.ID.String()] = struct{}{}
|
||||
f.addBlobHandle(pb.Handle())
|
||||
indexPackIDs[idStr] = struct{}{}
|
||||
}
|
||||
})
|
||||
@@ -552,13 +558,6 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
|
||||
delete(packIDs, id)
|
||||
}
|
||||
|
||||
if len(indexPackIDs) > 0 {
|
||||
list := make([]string, 0, len(indexPackIDs))
|
||||
for h := range indexPackIDs {
|
||||
list = append(list, h)
|
||||
}
|
||||
f.printer.E("some pack files are missing from the repository, getting their blobs from the repository index: %v\n\n", list)
|
||||
}
|
||||
return packIDs, nil
|
||||
}
|
||||
|
||||
@@ -569,16 +568,16 @@ func (f *Finder) findObjectPack(id string, t restic.BlobType) {
|
||||
return
|
||||
}
|
||||
|
||||
blobs := f.repo.LookupBlob(t, rid)
|
||||
blobs := f.repo.LookupBlob(restic.BlobHandle{Type: t, ID: rid})
|
||||
if len(blobs) == 0 {
|
||||
f.printer.S("Object %s with type %s not found in the index", t.String(), rid.Str())
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range blobs {
|
||||
if b.ID.Equal(rid) {
|
||||
f.printer.S("Object belongs to pack %s", b.PackID)
|
||||
f.printer.S(" ... Pack %s: %s", b.PackID.Str(), b.String())
|
||||
if b.Handle().ID.Equal(rid) {
|
||||
f.printer.S("Object belongs to pack %s", b.PackID())
|
||||
f.printer.S(" ... Pack %s: %v", b.PackID().String(), b.Handle())
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -599,7 +598,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
|
||||
return errors.Fatal("wrong number of arguments")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
var err error
|
||||
pat := findPattern{pattern: args}
|
||||
@@ -676,11 +675,15 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
|
||||
}
|
||||
|
||||
var filteredSnapshots []*data.Snapshot
|
||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots, printer) {
|
||||
err = opts.SnapshotFilter.FindAll(ctx, snapshotLister, repo, opts.Snapshots, func(_ string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filteredSnapshots = append(filteredSnapshots, sn)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Slice(filteredSnapshots, func(i, j int) bool {
|
||||
@@ -691,7 +694,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
|
||||
})
|
||||
|
||||
for _, sn := range filteredSnapshots {
|
||||
if f.blobIDs != nil || f.treeIDs != nil {
|
||||
if len(f.blobIDs) > 0 || len(f.treeIDs) > 0 {
|
||||
if err = f.findIDs(ctx, sn); err != nil && !errors.Is(err, errFindDone) {
|
||||
return err
|
||||
}
|
||||
@@ -703,7 +706,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
|
||||
}
|
||||
f.out.Finish()
|
||||
|
||||
if opts.ShowPackID && (f.blobIDs != nil || f.treeIDs != nil) {
|
||||
if opts.ShowPackID && (len(f.blobIDs) > 0 || len(f.treeIDs) > 0) {
|
||||
f.findObjectsPacks()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/backend"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts global.Options, pattern string) []byte {
|
||||
@@ -50,7 +51,7 @@ type testMatch struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
Size uint64 `json:"size,omitempty"`
|
||||
Date time.Time `json:"date,omitempty"`
|
||||
Date time.Time `json:"date"`
|
||||
UID uint32 `json:"uid,omitempty"`
|
||||
GID uint32 `json:"gid,omitempty"`
|
||||
}
|
||||
@@ -153,7 +154,7 @@ type JSONOutput struct {
|
||||
Path string `json:"path"`
|
||||
ParentTree string `json:"parent_tree,omitempty"`
|
||||
SnapshotID string `json:"snapshot"`
|
||||
Time time.Time `json:"time,omitempty"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
func TestFindPackfile(t *testing.T) {
|
||||
@@ -169,7 +170,7 @@ func TestFindPackfile(t *testing.T) {
|
||||
|
||||
// do all the testing wrapped inside withTermStatus()
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
@@ -179,9 +180,10 @@ func TestFindPackfile(t *testing.T) {
|
||||
|
||||
packID := restic.ID{}
|
||||
done := false
|
||||
err = repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
||||
if !done && pb.Type == restic.TreeBlob {
|
||||
packID = pb.PackID
|
||||
err = repo.ListBlobs(ctx, func(pb restic.PackBlob) {
|
||||
h := pb.Handle()
|
||||
if !done && h.Type == restic.TreeBlob {
|
||||
packID = pb.PackID()
|
||||
done = true
|
||||
}
|
||||
})
|
||||
@@ -228,20 +230,20 @@ func TestFindPackID(t *testing.T) {
|
||||
dataPackID := restic.ID{}
|
||||
treePackID := restic.ID{}
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
// load Index
|
||||
rtest.OK(t, repo.LoadIndex(ctx, nil))
|
||||
rtest.OK(t, repo.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
|
||||
// go through all index entries and collect data and tree packfile(s)
|
||||
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
||||
switch blob.Type {
|
||||
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackBlob) {
|
||||
switch blob.Handle().Type {
|
||||
case restic.DataBlob:
|
||||
dataPackID = blob.PackID
|
||||
dataPackID = blob.PackID()
|
||||
case restic.TreeBlob:
|
||||
treePackID = blob.PackID
|
||||
treePackID = blob.PackID()
|
||||
}
|
||||
}))
|
||||
return nil
|
||||
@@ -258,6 +260,28 @@ func TestFindPackID(t *testing.T) {
|
||||
rtest.Assert(t, len(findRes) == numberOfFiles, "expected %d entries for this packfile, got %d",
|
||||
numberOfFiles, len(findRes))
|
||||
|
||||
// corrupt the data pack file; find must fall back to the index
|
||||
be := captureBackend(&env.gopts)
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
h := backend.Handle{Type: backend.PackFile, Name: dataPackID.String()}
|
||||
rtest.OK(t, be().Remove(ctx, h))
|
||||
buf := make([]byte, 10)
|
||||
rtest.OK(t, be().Save(ctx, h, backend.NewByteReader(buf, be().Hasher())))
|
||||
return nil
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
out = testRunFind(t, true, FindOptions{PackID: true}, env.gopts, dataPackID.String())
|
||||
findRes = []JSONOutput{}
|
||||
rtest.OK(t, json.Unmarshal(out, &findRes))
|
||||
rtest.Assert(t, len(findRes) == numberOfFiles, "expected %d entries for broken packfile, got %d",
|
||||
numberOfFiles, len(findRes))
|
||||
|
||||
// look for tree packfile
|
||||
rtest.Assert(t, !treePackID.IsNull(), "expected to find tree packfile in repo")
|
||||
packID = treePackID.String()
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -189,7 +190,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for forget command")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -199,11 +200,15 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
var snapshots data.Snapshots
|
||||
removeSnIDs := restic.NewIDSet()
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
|
||||
err = opts.SnapshotFilter.FindAll(ctx, repo, repo, args, func(_ string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var jsonGroups []*ForgetGroup
|
||||
@@ -348,7 +353,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs))
|
||||
}
|
||||
pruneOptions.DryRun = opts.DryRun
|
||||
return runPruneWithRepo(ctx, pruneOptions, repo, removeSnIDs, printer)
|
||||
return runPruneWithRepo(ctx, pruneOptions, gopts, repo, removeSnIDs, printer)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -55,7 +56,7 @@ func (opts *generateOptions) AddFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
|
||||
}
|
||||
|
||||
func writeManpages(root *cobra.Command, dir string, printer progress.Printer) error {
|
||||
func writeManpages(root *cobra.Command, dir string, printer restic.Printer) error {
|
||||
// use a fixed date for the man pages so that generating them is deterministic
|
||||
date, err := time.Parse("Jan 2006", "Jan 2017")
|
||||
if err != nil {
|
||||
@@ -73,7 +74,7 @@ func writeManpages(root *cobra.Command, dir string, printer progress.Printer) er
|
||||
return doc.GenManTree(root, header, dir)
|
||||
}
|
||||
|
||||
func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts global.Options) (err error) {
|
||||
func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer restic.Printer, gopts global.Options) (err error) {
|
||||
printer.PT("writing %s completion file to %v", shell, filename)
|
||||
var outWriter io.Writer
|
||||
if filename != "-" {
|
||||
@@ -116,7 +117,7 @@ func runGenerate(opts generateOptions, gopts global.Options, args []string, term
|
||||
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
cmdRoot := newRootCommand(&global.Options{})
|
||||
|
||||
if opts.ManDir != "" {
|
||||
|
||||
@@ -60,7 +60,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts global.Options, args [
|
||||
return errors.Fatal("the init command expects no arguments, only options - please see `restic help init` for usage and flags")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
var version uint
|
||||
switch opts.RepositoryVersion {
|
||||
@@ -108,7 +108,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts global.Options, args [
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer progress.Printer) (*chunker.Pol, error) {
|
||||
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer restic.Printer) (*chunker.Pol, error) {
|
||||
if opts.CopyChunkerParameters {
|
||||
otherGopts, _, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "secondary")
|
||||
if err != nil {
|
||||
|
||||
@@ -10,13 +10,12 @@ import (
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func testRunInit(t testing.TB, gopts global.Options) {
|
||||
repository.TestUseLowSecurityKDFParameters(t)
|
||||
restic.TestDisableCheckPolynomial(t)
|
||||
restic.TestSetLockTimeout(t, 0)
|
||||
repository.TestSetLockTimeout(t, 0)
|
||||
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runInit(ctx, InitOptions{}, gopts, nil, gopts.Term)
|
||||
@@ -26,6 +25,7 @@ func testRunInit(t testing.TB, gopts global.Options) {
|
||||
|
||||
// create temporary junk files to verify that restic does not trip over them
|
||||
for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
|
||||
rtest.OK(t, os.MkdirAll(filepath.Join(gopts.Repo, path), 0700))
|
||||
rtest.OK(t, os.WriteFile(filepath.Join(gopts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
|
||||
}
|
||||
}
|
||||
@@ -57,14 +57,14 @@ func TestInitCopyChunkerParams(t *testing.T) {
|
||||
|
||||
var repo *repository.Repository
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
repo, err = global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||
repo, err = global.OpenRepository(ctx, gopts, restic.NewNoopPrinter())
|
||||
return err
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
var otherRepo *repository.Repository
|
||||
err = withTermStatus(t, env2.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
otherRepo, err = global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||
otherRepo, err = global.OpenRepository(ctx, gopts, restic.NewNoopPrinter())
|
||||
return err
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -60,7 +61,7 @@ func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, ar
|
||||
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -70,7 +71,7 @@ func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, ar
|
||||
return addKey(ctx, repo, gopts, opts, printer)
|
||||
}
|
||||
|
||||
func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer progress.Printer) error {
|
||||
func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer restic.Printer) error {
|
||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func testRunKeyListOtherIDs(t testing.TB, gopts global.Options) []string {
|
||||
@@ -63,11 +63,13 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts global.Options) {
|
||||
rtest.OK(t, err)
|
||||
|
||||
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||
repo, err := global.OpenRepository(ctx, gopts, restic.NewNoopPrinter())
|
||||
rtest.OK(t, err)
|
||||
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 2, "")
|
||||
err = repo.SearchKey(ctx, testKeyNewPassword, 2, "")
|
||||
rtest.OK(t, err)
|
||||
|
||||
key, err := repository.LoadKey(ctx, repo, repo.KeyID())
|
||||
rtest.OK(t, err)
|
||||
rtest.Equals(t, "john", key.Username)
|
||||
rtest.Equals(t, "example.com", key.Hostname)
|
||||
return nil
|
||||
@@ -105,11 +107,13 @@ func testRunKeyPasswdUserHost(t testing.TB, newPassword string, gopts global.Opt
|
||||
|
||||
gopts.Password = testKeyNewPassword
|
||||
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||
repo, err := global.OpenRepository(ctx, gopts, restic.NewNoopPrinter())
|
||||
rtest.OK(t, err)
|
||||
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 1, "")
|
||||
err = repo.SearchKey(ctx, testKeyNewPassword, 1, "")
|
||||
rtest.OK(t, err)
|
||||
|
||||
key, err := repository.LoadKey(ctx, repo, repo.KeyID())
|
||||
rtest.OK(t, err)
|
||||
rtest.Equals(t, "john", key.Username)
|
||||
rtest.Equals(t, "example.com", key.Hostname)
|
||||
return nil
|
||||
|
||||
@@ -46,7 +46,7 @@ func runKeyList(ctx context.Context, gopts global.Options, args []string, term u
|
||||
return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -56,7 +56,7 @@ func runKeyList(ctx context.Context, gopts global.Options, args []string, term u
|
||||
return listKeys(ctx, repo, gopts, printer)
|
||||
}
|
||||
|
||||
func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer progress.Printer) error {
|
||||
func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer restic.Printer) error {
|
||||
type keyInfo struct {
|
||||
Current bool `json:"current"`
|
||||
ID string `json:"id"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -55,7 +56,7 @@ func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptio
|
||||
return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -65,7 +66,7 @@ func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptio
|
||||
return changePassword(ctx, repo, gopts, opts, printer)
|
||||
}
|
||||
|
||||
func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer progress.Printer) error {
|
||||
func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer restic.Printer) error {
|
||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -43,7 +43,7 @@ func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term
|
||||
return fmt.Errorf("key remove expects one argument as the key id")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -53,7 +53,7 @@ func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term
|
||||
return deleteKey(ctx, repo, args[0], printer)
|
||||
}
|
||||
|
||||
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer progress.Printer) error {
|
||||
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer restic.Printer) error {
|
||||
id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+59
-19
@@ -2,13 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository/index"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -18,10 +21,12 @@ func newListCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [flags] [" + listAllowedArgsUseString + "]",
|
||||
Use: "list [flags] [" + listAllowedArgsUseString + "|packs snapshotID]",
|
||||
Short: "List objects in the repository",
|
||||
Long: `
|
||||
The "list" command allows listing objects in the repository based on type.
|
||||
The "list packs snapshotID" variant accepts one snapshotID and lists all packfiles
|
||||
used by this snapshot.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -35,19 +40,18 @@ Exit status is 12 if the password is incorrect.
|
||||
DisableAutoGenTag: true,
|
||||
GroupID: cmdGroupDefault,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||
return runList(cmd.Context(), *globalOptions, args, globalOptions.Term, listAllowedArgsUseString)
|
||||
},
|
||||
ValidArgs: listAllowedArgs,
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal, listAllowedArgsUseString string) error {
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.Fatal("type not specified")
|
||||
if len(args) == 0 || (args[0] == "packs" && len(args) > 2) || (args[0] != "packs" && len(args) != 1) {
|
||||
return errors.Fatal(fmt.Sprintf("too many parameters or type not specified. Must be one of [%s] or 'packs snapshotID'", listAllowedArgsUseString))
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks", printer)
|
||||
@@ -60,6 +64,10 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T
|
||||
switch args[0] {
|
||||
case "packs":
|
||||
t = restic.PackFile
|
||||
if len(args) == 2 {
|
||||
// args[1] needs to be a snapshotID
|
||||
return packfileList(ctx, repo, args[1], printer)
|
||||
}
|
||||
case "index":
|
||||
t = restic.IndexFile
|
||||
case "snapshots":
|
||||
@@ -69,18 +77,13 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T
|
||||
case "locks":
|
||||
t = restic.LockFile
|
||||
case "blobs":
|
||||
return index.ForAllIndexes(ctx, repo, repo, func(_ restic.ID, idx *index.Index, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
for entry := range repository.AllIndexBlobs(ctx, repo, repo) {
|
||||
if entry.Error != nil {
|
||||
return entry.Error
|
||||
}
|
||||
for blobs := range idx.Values() {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
printer.S("%v %v", blobs.Type, blobs.ID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
printer.S("%v %v", entry.Handle.Type, entry.Handle.ID)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.Fatal("invalid type")
|
||||
}
|
||||
@@ -90,3 +93,40 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// packfileList handles the list packs <snapshotID> variant.
|
||||
// It prints a sorted list of packfiles belonging to this snapshot.
|
||||
func packfileList(ctx context.Context, repo restic.Repository, snapshotID string, printer restic.Printer) error {
|
||||
// ignore subpaths as this command is intended to list all packfiles necessary to restore the snapshot
|
||||
// subpaths would require special handling and limit restorability
|
||||
sn, _, err := (&data.SnapshotFilter{}).FindLatest(ctx, repo, repo, snapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find snapshot: %v", err)
|
||||
}
|
||||
|
||||
if err = repo.LoadIndex(ctx, printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usedBlobs := repo.NewAssociatedBlobSet()
|
||||
bar := printer.NewCounter("snapshot")
|
||||
bar.SetMax(uint64(1))
|
||||
err = data.FindUsedBlobs(ctx, repo, []restic.ID{*sn.Tree}, usedBlobs, bar)
|
||||
bar.Done()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
snapPacks := restic.NewIDSet()
|
||||
for bh := range usedBlobs.Keys() {
|
||||
for _, blob := range repo.LookupBlob(bh) {
|
||||
snapPacks.Insert(blob.PackID())
|
||||
}
|
||||
}
|
||||
|
||||
for _, packID := range snapPacks.List() {
|
||||
printer.S("%v", packID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func testRunList(t testing.TB, gopts global.Options, tpe string) restic.IDs {
|
||||
func testRunList(t testing.TB, gopts global.Options, params ...string) restic.IDs {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runList(ctx, gopts, []string{tpe}, gopts.Term)
|
||||
return runList(ctx, gopts, params, gopts.Term, "")
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
return parseIDsFromReader(t, buf)
|
||||
@@ -61,18 +61,18 @@ func testListSnapshots(t testing.TB, gopts global.Options, expected int) restic.
|
||||
// extract blob set from repository index
|
||||
func testListBlobs(t testing.TB, gopts global.Options) (blobSetFromIndex restic.IDSet) {
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
// make sure the index is loaded
|
||||
rtest.OK(t, repo.LoadIndex(ctx, nil))
|
||||
rtest.OK(t, repo.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
|
||||
|
||||
// get blobs from index
|
||||
blobSetFromIndex = restic.NewIDSet()
|
||||
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
||||
blobSetFromIndex.Insert(blob.ID)
|
||||
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackBlob) {
|
||||
blobSetFromIndex.Insert(blob.Handle().ID)
|
||||
}))
|
||||
return nil
|
||||
})
|
||||
@@ -102,3 +102,24 @@ func TestListBlobs(t *testing.T) {
|
||||
|
||||
rtest.Assert(t, blobSetFromIndex.Equals(testIDSet), "the set of restic.ID s should be equal")
|
||||
}
|
||||
|
||||
func TestPackfileListWithSnapshot(t *testing.T) {
|
||||
// setup
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
testSetupBackupData(t, env)
|
||||
|
||||
// 3 backups, single file each
|
||||
opts := BackupOptions{}
|
||||
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "40")}, opts, env.gopts)
|
||||
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "41")}, opts, env.gopts)
|
||||
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "42")}, opts, env.gopts)
|
||||
testListSnapshots(t, env.gopts, 3)
|
||||
|
||||
// run packfilelist
|
||||
packfiles := testRunList(t, env.gopts, "packs")
|
||||
rtest.Assert(t, len(packfiles) == 6, "expected 6 packfiles in repository, got %d", len(packfiles))
|
||||
|
||||
packfiles = testRunList(t, env.gopts, "packs", "latest")
|
||||
rtest.Assert(t, len(packfiles) == 2, "expected 2 packfiles in snapshot, got %d", len(packfiles))
|
||||
}
|
||||
|
||||
+78
-69
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
)
|
||||
|
||||
@@ -93,7 +94,7 @@ func (opts *LsOptions) AddFlags(f *pflag.FlagSet) {
|
||||
|
||||
type lsPrinter interface {
|
||||
Snapshot(sn *data.Snapshot) error
|
||||
Node(path string, node *data.Node, isPrefixDirectory bool) error
|
||||
NodeOutput(n lsNodeOutput, isPrefixDirectory bool) error
|
||||
LeaveDir(path string) error
|
||||
Close() error
|
||||
}
|
||||
@@ -120,55 +121,68 @@ func (p *jsonLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||
})
|
||||
}
|
||||
|
||||
// Node formats node in our custom JSON format, followed by a newline.
|
||||
func (p *jsonLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||
if isPrefixDirectory {
|
||||
return nil
|
||||
}
|
||||
return lsNodeJSON(p.enc, path, node)
|
||||
type lsNodeOutput struct {
|
||||
Name string `json:"name"`
|
||||
Type data.NodeType `json:"type"`
|
||||
Path string `json:"path"`
|
||||
UID uint32 `json:"uid"`
|
||||
GID uint32 `json:"gid"`
|
||||
Size *uint64 `json:"size,omitempty"`
|
||||
Mode os.FileMode `json:"mode,omitempty"`
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
ModTime time.Time `json:"mtime"`
|
||||
AccessTime time.Time `json:"atime"`
|
||||
ChangeTime time.Time `json:"ctime"`
|
||||
Inode uint64 `json:"inode,omitempty"`
|
||||
MessageType string `json:"message_type"`
|
||||
StructType string `json:"struct_type"`
|
||||
|
||||
// Used for the ncdu output only
|
||||
LinkTarget string `json:"-"`
|
||||
DeviceID uint64 `json:"-"`
|
||||
Links uint64 `json:"-"`
|
||||
}
|
||||
|
||||
func lsNodeJSON(enc *json.Encoder, path string, node *data.Node) error {
|
||||
n := &struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path"`
|
||||
UID uint32 `json:"uid"`
|
||||
GID uint32 `json:"gid"`
|
||||
Size *uint64 `json:"size,omitempty"`
|
||||
Mode os.FileMode `json:"mode,omitempty"`
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
ModTime time.Time `json:"mtime,omitempty"`
|
||||
AccessTime time.Time `json:"atime,omitempty"`
|
||||
ChangeTime time.Time `json:"ctime,omitempty"`
|
||||
Inode uint64 `json:"inode,omitempty"`
|
||||
MessageType string `json:"message_type"` // "node"
|
||||
StructType string `json:"struct_type"` // "node", deprecated
|
||||
func (n lsNodeOutput) fileSize() uint64 {
|
||||
if n.Size == nil {
|
||||
return 0
|
||||
}
|
||||
return *n.Size
|
||||
}
|
||||
|
||||
size uint64 // Target for Size pointer.
|
||||
}{
|
||||
func lsNodeOutputFrom(path string, node *data.Node) lsNodeOutput {
|
||||
n := lsNodeOutput{
|
||||
Name: node.Name,
|
||||
Type: string(node.Type),
|
||||
Type: node.Type,
|
||||
Path: path,
|
||||
UID: node.UID,
|
||||
GID: node.GID,
|
||||
size: node.Size,
|
||||
Mode: node.Mode,
|
||||
Permissions: node.Mode.String(),
|
||||
ModTime: node.ModTime,
|
||||
AccessTime: node.AccessTime,
|
||||
ChangeTime: node.ChangeTime,
|
||||
Inode: node.Inode,
|
||||
LinkTarget: node.LinkTarget,
|
||||
DeviceID: node.DeviceID,
|
||||
Links: node.Links,
|
||||
MessageType: "node",
|
||||
StructType: "node",
|
||||
}
|
||||
// Always print size for regular files, even when empty,
|
||||
// but never for other types.
|
||||
if node.Type == data.NodeTypeFile {
|
||||
n.Size = &n.size
|
||||
size := node.Size
|
||||
n.Size = &size
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
return enc.Encode(n)
|
||||
func (p *jsonLsPrinter) NodeOutput(n lsNodeOutput, isPrefixDirectory bool) error {
|
||||
if isPrefixDirectory {
|
||||
return nil
|
||||
}
|
||||
return p.enc.Encode(n)
|
||||
}
|
||||
|
||||
func (p *jsonLsPrinter) LeaveDir(_ string) error { return nil }
|
||||
@@ -195,7 +209,7 @@ func (p *ncduLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
|
||||
func lsNcduOutput(node lsNodeOutput) ([]byte, error) {
|
||||
type NcduNode struct {
|
||||
Name string `json:"name"`
|
||||
Asize uint64 `json:"asize"`
|
||||
@@ -212,11 +226,12 @@ func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
|
||||
|
||||
const blockSize = 512
|
||||
|
||||
size := node.fileSize()
|
||||
outNode := NcduNode{
|
||||
Name: node.Name,
|
||||
Asize: node.Size,
|
||||
Asize: size,
|
||||
// round up to nearest full blocksize
|
||||
Dsize: (node.Size + blockSize - 1) / blockSize * blockSize,
|
||||
Dsize: (size + blockSize - 1) / blockSize * blockSize,
|
||||
Dev: node.DeviceID,
|
||||
Ino: node.Inode,
|
||||
NLink: node.Links,
|
||||
@@ -244,8 +259,8 @@ func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
|
||||
return json.Marshal(outNode)
|
||||
}
|
||||
|
||||
func (p *ncduLsPrinter) Node(path string, node *data.Node, _ bool) error {
|
||||
out, err := lsNcduNode(path, node)
|
||||
func (p *ncduLsPrinter) NodeOutput(node lsNodeOutput, _ bool) error {
|
||||
out, err := lsNcduOutput(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -275,8 +290,8 @@ type textLsPrinter struct {
|
||||
ListLong bool
|
||||
HumanReadable bool
|
||||
termPrinter interface {
|
||||
P(msg string, args ...interface{})
|
||||
S(msg string, args ...interface{})
|
||||
P(msg string, args ...any)
|
||||
S(msg string, args ...any)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,9 +299,9 @@ func (p *textLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||
p.termPrinter.P("%v filtered by %v:", sn, p.dirs)
|
||||
return nil
|
||||
}
|
||||
func (p *textLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||
func (p *textLsPrinter) NodeOutput(node lsNodeOutput, isPrefixDirectory bool) error {
|
||||
if !isPrefixDirectory {
|
||||
p.termPrinter.S("%s", formatNode(path, node, p.ListLong, p.HumanReadable))
|
||||
p.termPrinter.S("%s", formatNodeOutput(node, p.ListLong, p.HumanReadable))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -298,14 +313,8 @@ func (p *textLsPrinter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// for ls -l output sorting
|
||||
type toSortOutput struct {
|
||||
nodepath string
|
||||
node *data.Node
|
||||
}
|
||||
|
||||
func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
termPrinter := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
termPrinter := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
if len(args) == 0 {
|
||||
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
|
||||
@@ -431,7 +440,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
|
||||
printedDir := false
|
||||
if withinDir(nodepath) {
|
||||
// if we're within a target path, print the node
|
||||
if err := printer.Node(nodepath, node, false); err != nil {
|
||||
if err := printer.NodeOutput(lsNodeOutputFrom(nodepath, node), false); err != nil {
|
||||
return err
|
||||
}
|
||||
printedDir = true
|
||||
@@ -448,7 +457,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
|
||||
if approachingMatchingTree(nodepath) {
|
||||
// print node leading up to the target paths
|
||||
if !printedDir {
|
||||
return printer.Node(nodepath, node, true)
|
||||
return printer.NodeOutput(lsNodeOutputFrom(nodepath, node), true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -487,7 +496,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
|
||||
|
||||
type sortedPrinter struct {
|
||||
printer lsPrinter
|
||||
collector []toSortOutput
|
||||
collector []lsNodeOutput
|
||||
sortMode SortMode
|
||||
reverse bool
|
||||
}
|
||||
@@ -495,9 +504,9 @@ type sortedPrinter struct {
|
||||
func (p *sortedPrinter) Snapshot(sn *data.Snapshot) error {
|
||||
return p.printer.Snapshot(sn)
|
||||
}
|
||||
func (p *sortedPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||
func (p *sortedPrinter) NodeOutput(node lsNodeOutput, isPrefixDirectory bool) error {
|
||||
if !isPrefixDirectory {
|
||||
p.collector = append(p.collector, toSortOutput{path, node})
|
||||
p.collector = append(p.collector, node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -506,49 +515,49 @@ func (p *sortedPrinter) LeaveDir(_ string) error {
|
||||
return nil
|
||||
}
|
||||
func (p *sortedPrinter) Close() error {
|
||||
var comparator func(a, b toSortOutput) int
|
||||
var comparator func(a, b lsNodeOutput) int
|
||||
switch p.sortMode {
|
||||
case SortModeName:
|
||||
case SortModeSize:
|
||||
comparator = func(a, b toSortOutput) int {
|
||||
comparator = func(a, b lsNodeOutput) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(a.node.Size, b.node.Size),
|
||||
cmp.Compare(a.nodepath, b.nodepath),
|
||||
cmp.Compare(a.fileSize(), b.fileSize()),
|
||||
cmp.Compare(a.Path, b.Path),
|
||||
)
|
||||
}
|
||||
case SortModeMtime:
|
||||
comparator = func(a, b toSortOutput) int {
|
||||
comparator = func(a, b lsNodeOutput) int {
|
||||
return cmp.Or(
|
||||
a.node.ModTime.Compare(b.node.ModTime),
|
||||
cmp.Compare(a.nodepath, b.nodepath),
|
||||
a.ModTime.Compare(b.ModTime),
|
||||
cmp.Compare(a.Path, b.Path),
|
||||
)
|
||||
}
|
||||
case SortModeAtime:
|
||||
comparator = func(a, b toSortOutput) int {
|
||||
comparator = func(a, b lsNodeOutput) int {
|
||||
return cmp.Or(
|
||||
a.node.AccessTime.Compare(b.node.AccessTime),
|
||||
cmp.Compare(a.nodepath, b.nodepath),
|
||||
a.AccessTime.Compare(b.AccessTime),
|
||||
cmp.Compare(a.Path, b.Path),
|
||||
)
|
||||
}
|
||||
case SortModeCtime:
|
||||
comparator = func(a, b toSortOutput) int {
|
||||
comparator = func(a, b lsNodeOutput) int {
|
||||
return cmp.Or(
|
||||
a.node.ChangeTime.Compare(b.node.ChangeTime),
|
||||
cmp.Compare(a.nodepath, b.nodepath),
|
||||
a.ChangeTime.Compare(b.ChangeTime),
|
||||
cmp.Compare(a.Path, b.Path),
|
||||
)
|
||||
}
|
||||
case SortModeExt:
|
||||
// map name to extension
|
||||
mapExt := make(map[string]string, len(p.collector))
|
||||
for _, item := range p.collector {
|
||||
ext := filepath.Ext(item.nodepath)
|
||||
mapExt[item.nodepath] = ext
|
||||
ext := filepath.Ext(item.Path)
|
||||
mapExt[item.Path] = ext
|
||||
}
|
||||
|
||||
comparator = func(a, b toSortOutput) int {
|
||||
comparator = func(a, b lsNodeOutput) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(mapExt[a.nodepath], mapExt[b.nodepath]),
|
||||
cmp.Compare(a.nodepath, b.nodepath),
|
||||
cmp.Compare(mapExt[a.Path], mapExt[b.Path]),
|
||||
cmp.Compare(a.Path, b.Path),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -560,7 +569,7 @@ func (p *sortedPrinter) Close() error {
|
||||
slices.Reverse(p.collector)
|
||||
}
|
||||
for _, elem := range p.collector {
|
||||
if err := p.printer.Node(elem.nodepath, elem.node, false); err != nil {
|
||||
if err := p.printer.NodeOutput(elem, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -96,12 +96,12 @@ func TestLsNodeJSON(t *testing.T) {
|
||||
c := lsTestNodes[i]
|
||||
buf := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(buf)
|
||||
err := lsNodeJSON(enc, c.path, &c.Node)
|
||||
err := enc.Encode(lsNodeOutputFrom(c.path, &c.Node))
|
||||
rtest.OK(t, err)
|
||||
rtest.Equals(t, expect+"\n", buf.String())
|
||||
|
||||
// Sanity check: output must be valid JSON.
|
||||
var v interface{}
|
||||
var v any
|
||||
err = json.NewDecoder(buf).Decode(&v)
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
@@ -116,12 +116,12 @@ func TestLsNcduNode(t *testing.T) {
|
||||
`{"name":"sticky","asize":0,"dsize":0,"dev":0,"ino":0,"nlink":0,"notreg":false,"uid":0,"gid":0,"mode":4077,"mtime":0}`,
|
||||
} {
|
||||
c := lsTestNodes[i]
|
||||
out, err := lsNcduNode(c.path, &c.Node)
|
||||
out, err := lsNcduOutput(lsNodeOutputFrom(c.path, &c.Node))
|
||||
rtest.OK(t, err)
|
||||
rtest.Equals(t, expect, string(out))
|
||||
|
||||
// Sanity check: output must be valid JSON.
|
||||
var v interface{}
|
||||
var v any
|
||||
err = json.Unmarshal(out, &v)
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
@@ -138,24 +138,24 @@ func TestLsNcdu(t *testing.T) {
|
||||
Hostname: "host",
|
||||
Paths: []string{"/example"},
|
||||
}))
|
||||
rtest.OK(t, printer.Node("/directory", &data.Node{
|
||||
rtest.OK(t, printer.NodeOutput(lsNodeOutputFrom("/directory", &data.Node{
|
||||
Type: data.NodeTypeDir,
|
||||
Name: "directory",
|
||||
ModTime: modTime,
|
||||
}, false))
|
||||
rtest.OK(t, printer.Node("/directory/data", &data.Node{
|
||||
}), false))
|
||||
rtest.OK(t, printer.NodeOutput(lsNodeOutputFrom("/directory/data", &data.Node{
|
||||
Type: data.NodeTypeFile,
|
||||
Name: "data",
|
||||
Size: 42,
|
||||
ModTime: modTime,
|
||||
}, false))
|
||||
}), false))
|
||||
rtest.OK(t, printer.LeaveDir("/directory"))
|
||||
rtest.OK(t, printer.Node("/file", &data.Node{
|
||||
rtest.OK(t, printer.NodeOutput(lsNodeOutputFrom("/file", &data.Node{
|
||||
Type: data.NodeTypeFile,
|
||||
Name: "file",
|
||||
Size: 12345,
|
||||
ModTime: modTime,
|
||||
}, false))
|
||||
}), false))
|
||||
rtest.OK(t, printer.Close())
|
||||
|
||||
rtest.Equals(t, `[1, 2, {"time":"0001-01-01T00:00:00Z","tree":null,"paths":["/example"],"hostname":"host"}, [{"name":"/"},
|
||||
|
||||
@@ -53,7 +53,7 @@ func (opts *MigrateOptions) AddFlags(f *pflag.FlagSet) {
|
||||
f.BoolVarP(&opts.Force, "force", "f", false, `apply a migration a second time`)
|
||||
}
|
||||
|
||||
func checkMigrations(ctx context.Context, repo restic.Repository, printer progress.Printer) error {
|
||||
func checkMigrations(ctx context.Context, repo restic.Repository, printer restic.Printer) error {
|
||||
printer.P("available migrations:\n")
|
||||
found := false
|
||||
|
||||
@@ -76,7 +76,7 @@ func checkMigrations(ctx context.Context, repo restic.Repository, printer progre
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Options, repo restic.Repository, args []string, term ui.Terminal, printer progress.Printer) error {
|
||||
func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Options, repo restic.Repository, args []string, term ui.Terminal, printer restic.Printer) error {
|
||||
var firsterr error
|
||||
for _, name := range args {
|
||||
found := false
|
||||
@@ -135,7 +135,7 @@ func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Opti
|
||||
}
|
||||
|
||||
func runMigrate(ctx context.Context, opts MigrateOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
|
||||
+8
-20
@@ -19,13 +19,15 @@ import (
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
|
||||
"github.com/restic/restic/internal/fuse"
|
||||
|
||||
systemFuse "github.com/anacrolix/fuse"
|
||||
"github.com/anacrolix/fuse/fs"
|
||||
fusefs "github.com/anacrolix/fuse/fs"
|
||||
)
|
||||
|
||||
func registerMountCommand(cmdRoot *cobra.Command, globalOptions *global.Options) {
|
||||
@@ -119,7 +121,7 @@ func (opts *MountOptions) AddFlags(f *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if opts.TimeTemplate == "" {
|
||||
return errors.Fatal("time template string cannot be empty")
|
||||
@@ -169,7 +171,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args
|
||||
}
|
||||
}
|
||||
|
||||
systemFuse.Debug = func(msg interface{}) {
|
||||
systemFuse.Debug = func(msg any) {
|
||||
debug.Log("fuse: %v", msg)
|
||||
}
|
||||
|
||||
@@ -196,7 +198,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
err = fs.Serve(c, root)
|
||||
err = fusefs.Serve(c, root)
|
||||
}()
|
||||
|
||||
printer.S("Now serving the repository at %s", mountpoint)
|
||||
@@ -270,9 +272,9 @@ func checkMountpointOverlap(repoPath, mountpoint string) error {
|
||||
switch {
|
||||
case rp == mp:
|
||||
return errors.Fatal(fmt.Sprintf("mountpoint %s is the local repository directory%s", mp, tail))
|
||||
case isInside(rp, mp):
|
||||
case fs.HasPathPrefix(rp, mp):
|
||||
return errors.Fatal(fmt.Sprintf("mountpoint %s is inside the local repository directory %s%s", mp, rp, tail))
|
||||
case isInside(mp, rp):
|
||||
case fs.HasPathPrefix(mp, rp):
|
||||
return errors.Fatal(fmt.Sprintf("local repository directory %s is inside the mountpoint %s%s", rp, mp, tail))
|
||||
}
|
||||
return nil
|
||||
@@ -293,17 +295,3 @@ func resolvePath(p string) (string, error) {
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// isInside reports whether child is strictly nested inside parent. Both paths
|
||||
// must already be cleaned and absolute. Equal paths return false; the caller
|
||||
// handles equality separately so it can produce a distinct error message.
|
||||
func isInside(parent, child string) bool {
|
||||
rel, err := filepath.Rel(parent, child)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if rel == "." || rel == ".." {
|
||||
return false
|
||||
}
|
||||
return !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
||||
}
|
||||
|
||||
@@ -14,16 +14,15 @@ import (
|
||||
|
||||
systemFuse "github.com/anacrolix/fuse"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
const (
|
||||
mountWait = 20
|
||||
mountSleep = 100 * time.Millisecond
|
||||
mountWait = 400
|
||||
mountSleep = 5 * time.Millisecond
|
||||
mountTestSubdir = "snapshots"
|
||||
)
|
||||
|
||||
@@ -47,7 +46,7 @@ func snapshotsDirExists(t testing.TB, dir string) bool {
|
||||
// waitForMount blocks (max mountWait * mountSleep) until the subdir
|
||||
// "snapshots" appears in the dir.
|
||||
func waitForMount(t testing.TB, dir string) {
|
||||
for i := 0; i < mountWait; i++ {
|
||||
for range mountWait {
|
||||
if snapshotsDirExists(t, dir) {
|
||||
t.Log("mounted directory is ready")
|
||||
return
|
||||
@@ -71,7 +70,7 @@ func testRunMount(t testing.TB, gopts global.Options, dir string, wg *sync.WaitG
|
||||
|
||||
func testRunUmount(t testing.TB, dir string) {
|
||||
var err error
|
||||
for i := 0; i < mountWait; i++ {
|
||||
for range mountWait {
|
||||
if err = systemFuse.Unmount(dir); err == nil {
|
||||
t.Logf("directory %v umounted", dir)
|
||||
return
|
||||
@@ -131,7 +130,7 @@ func checkSnapshots(t testing.TB, gopts global.Options, mountpoint string, snaps
|
||||
}
|
||||
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -274,11 +273,6 @@ func TestMountSameTimestamps(t *testing.T) {
|
||||
t.Skip("Skipping fuse tests")
|
||||
}
|
||||
|
||||
debugEnabled := debug.TestLogToStderr(t)
|
||||
if debugEnabled {
|
||||
defer debug.TestDisableLog(t)
|
||||
}
|
||||
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
// must list snapshots more than once
|
||||
env.gopts.BackendTestHook = nil
|
||||
|
||||
+18
-19
@@ -175,7 +175,7 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts global.Options, term
|
||||
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for prune command")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -190,11 +190,11 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts global.Options, term
|
||||
opts.unsafeRecovery = true
|
||||
}
|
||||
|
||||
return runPruneWithRepo(ctx, opts, repo, restic.NewIDSet(), printer)
|
||||
return runPruneWithRepo(ctx, opts, gopts, repo, restic.NewIDSet(), printer)
|
||||
}
|
||||
|
||||
func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer progress.Printer) error {
|
||||
if repo.Cache() == nil {
|
||||
func runPruneWithRepo(ctx context.Context, opts PruneOptions, gopts global.Options, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer restic.Printer) error {
|
||||
if repo.Cache() == nil && !gopts.JSON {
|
||||
printer.S("warning: running prune without a cache, this may be very slow!")
|
||||
}
|
||||
|
||||
@@ -230,9 +230,13 @@ func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.R
|
||||
printer.P("\nWould have made the following changes:")
|
||||
}
|
||||
|
||||
err = printPruneStats(printer, plan.Stats())
|
||||
if err != nil {
|
||||
return err
|
||||
if !gopts.JSON {
|
||||
err = printPruneStats(printer, plan.Stats())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
gopts.Term.Print(ui.ToJSONString(plan.Stats()))
|
||||
}
|
||||
|
||||
// Trigger GC to reset garbage collection threshold
|
||||
@@ -242,7 +246,7 @@ func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.R
|
||||
}
|
||||
|
||||
// printPruneStats prints out the statistics
|
||||
func printPruneStats(printer progress.Printer, stats repository.PruneStats) error {
|
||||
func printPruneStats(printer restic.Printer, stats repository.PruneStats) error {
|
||||
printer.V("\nused: %10d blobs / %s", stats.Blobs.Used, ui.FormatBytes(stats.Size.Used))
|
||||
if stats.Blobs.Duplicate > 0 {
|
||||
printer.V("duplicates: %10d blobs / %s", stats.Blobs.Duplicate, ui.FormatBytes(stats.Size.Duplicate))
|
||||
@@ -251,24 +255,19 @@ func printPruneStats(printer progress.Printer, stats repository.PruneStats) erro
|
||||
if stats.Size.Unref > 0 {
|
||||
printer.V("unreferenced: %s", ui.FormatBytes(stats.Size.Unref))
|
||||
}
|
||||
totalBlobs := stats.Blobs.Used + stats.Blobs.Unused + stats.Blobs.Duplicate
|
||||
totalSize := stats.Size.Used + stats.Size.Duplicate + stats.Size.Unused + stats.Size.Unref
|
||||
unusedSize := stats.Size.Duplicate + stats.Size.Unused
|
||||
printer.V("total: %10d blobs / %s", totalBlobs, ui.FormatBytes(totalSize))
|
||||
printer.V("unused size: %s of total size", ui.FormatPercent(unusedSize, totalSize))
|
||||
printer.V("total: %10d blobs / %s", stats.Blobs.Total, ui.FormatBytes(stats.Size.Total))
|
||||
printer.V("unused size: %s of total size", ui.FormatPercent(stats.Size.Duplicate+stats.Size.Unused, stats.Size.Total))
|
||||
|
||||
printer.P("\nto repack: %10d blobs / %s", stats.Blobs.Repack, ui.FormatBytes(stats.Size.Repack))
|
||||
printer.P("this removes: %10d blobs / %s", stats.Blobs.Repackrm, ui.FormatBytes(stats.Size.Repackrm))
|
||||
printer.P("to delete: %10d blobs / %s", stats.Blobs.Remove, ui.FormatBytes(stats.Size.Remove+stats.Size.Unref))
|
||||
totalPruneSize := stats.Size.Remove + stats.Size.Repackrm + stats.Size.Unref
|
||||
printer.P("total prune: %10d blobs / %s", stats.Blobs.Remove+stats.Blobs.Repackrm, ui.FormatBytes(totalPruneSize))
|
||||
printer.P("total prune: %10d blobs / %s", stats.Blobs.RemoveTotal, ui.FormatBytes(stats.Size.RemoveTotal))
|
||||
if stats.Size.Uncompressed > 0 {
|
||||
printer.P("not yet compressed: %s", ui.FormatBytes(stats.Size.Uncompressed))
|
||||
}
|
||||
printer.P("remaining: %10d blobs / %s", totalBlobs-(stats.Blobs.Remove+stats.Blobs.Repackrm), ui.FormatBytes(totalSize-totalPruneSize))
|
||||
unusedAfter := unusedSize - stats.Size.Remove - stats.Size.Repackrm
|
||||
printer.P("remaining: %10d blobs / %s", stats.Blobs.Remain, ui.FormatBytes(stats.Size.Remain))
|
||||
printer.P("unused size after prune: %s (%s of remaining size)",
|
||||
ui.FormatBytes(unusedAfter), ui.FormatPercent(unusedAfter, totalSize-totalPruneSize))
|
||||
ui.FormatBytes(stats.Size.RemainUnused), ui.FormatPercent(stats.Size.RemainUnused, stats.Size.Remain))
|
||||
printer.P("")
|
||||
printer.V("totally used packs: %10d", stats.Packs.Used)
|
||||
printer.V("partly used packs: %10d", stats.Packs.PartlyUsed)
|
||||
@@ -283,7 +282,7 @@ func printPruneStats(printer progress.Printer, stats repository.PruneStats) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet, ignoreSnapshots restic.IDSet, printer progress.Printer) error {
|
||||
func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet, ignoreSnapshots restic.IDSet, printer restic.Printer) error {
|
||||
var snapshotTrees restic.IDs
|
||||
printer.P("loading all snapshots...")
|
||||
err := data.ForAllSnapshots(ctx, repo, repo, ignoreSnapshots,
|
||||
|
||||
@@ -258,3 +258,28 @@ func TestPruneRepackSmallerThanSmoke(t *testing.T) {
|
||||
MaxUnused: "5%",
|
||||
})
|
||||
}
|
||||
|
||||
func TestPruneJSON(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
createPrunableRepo(t, env)
|
||||
|
||||
buf, err := withCaptureStdout(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
gopts.JSON = true
|
||||
oldHook := gopts.BackendTestHook
|
||||
gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) { return newListOnceBackend(r), nil }
|
||||
defer func() {
|
||||
gopts.BackendTestHook = oldHook
|
||||
}()
|
||||
return runPrune(ctx, pruneDefaultOptions, gopts, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
var stats repository.PruneStats
|
||||
rtest.OK(t, json.Unmarshal(buf.Bytes(), &stats))
|
||||
|
||||
rtest.Equals(t, "summary", stats.MessageType)
|
||||
rtest.Assert(t, stats.Blobs.Total > 0, "expected non-zero total blobs, got %v", stats.Blobs.Total)
|
||||
rtest.Assert(t, stats.Packs.Total > 0, "expected non-zero total packs, got %v", stats.Packs.Total)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) err
|
||||
return err
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -75,9 +75,10 @@ func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) err
|
||||
// tree. If it is not referenced, we have a root tree.
|
||||
trees := make(map[restic.ID]bool)
|
||||
|
||||
err = repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
||||
if blob.Type == restic.TreeBlob {
|
||||
trees[blob.Blob.ID] = false
|
||||
err = repo.ListBlobs(ctx, func(blob restic.PackBlob) {
|
||||
h := blob.Handle()
|
||||
if h.Type == restic.TreeBlob {
|
||||
trees[h.ID] = false
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
@@ -173,7 +174,7 @@ func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) err
|
||||
|
||||
}
|
||||
|
||||
func createSnapshot(ctx context.Context, printer progress.Printer, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error {
|
||||
func createSnapshot(ctx context.Context, printer restic.Printer, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error {
|
||||
sn, err := data.NewSnapshot([]string{name}, tags, hostname, time.Now())
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -70,7 +71,7 @@ func newRebuildIndexCommand(globalOptions *global.Options) *cobra.Command {
|
||||
}
|
||||
|
||||
func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts global.Options, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/restic/restic/internal/backend"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository/index"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
@@ -24,13 +24,14 @@ func testRunRebuildIndex(t testing.TB, gopts global.Options) {
|
||||
}
|
||||
|
||||
func testRebuildIndex(t *testing.T, backendTestHook global.BackendWrapper) {
|
||||
repository.TestInjectKey(t, restic.TestParseID("b9883c60bed42db51be171ca52f055104b6ea7cfa2bc381c05b2b1f78231280c"), `{"mac":{"k":"maQ4ILA872XnDxHVEno94A==","r":"OptMBABwkgIsMQcHME8cBw=="},"encrypt":"janrR1efN7HyQ8kOZ9zhHixooZ/e+WelH0mT4v9WskQ="}`)
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
|
||||
rtest.SetupTarTestFixture(t, env.base, datafile)
|
||||
|
||||
out, err := testRunCheckOutput(t, env.gopts, false)
|
||||
out, _, err := testRunCheckOutput(t, env.gopts, false)
|
||||
if !strings.Contains(out, "contained in several indexes") {
|
||||
t.Fatalf("did not find checker hint for packs in several indexes")
|
||||
}
|
||||
@@ -47,7 +48,7 @@ func testRebuildIndex(t *testing.T, backendTestHook global.BackendWrapper) {
|
||||
testRunRebuildIndex(t, env.gopts)
|
||||
|
||||
env.gopts.BackendTestHook = nil
|
||||
out, err = testRunCheckOutput(t, env.gopts, false)
|
||||
out, _, err = testRunCheckOutput(t, env.gopts, false)
|
||||
if len(out) != 0 {
|
||||
t.Fatalf("expected no output from the checker, got: %v", out)
|
||||
}
|
||||
@@ -61,15 +62,6 @@ func TestRebuildIndex(t *testing.T) {
|
||||
testRebuildIndex(t, nil)
|
||||
}
|
||||
|
||||
func TestRebuildIndexAlwaysFull(t *testing.T) {
|
||||
indexFull := index.Full
|
||||
defer func() {
|
||||
index.Full = indexFull
|
||||
}()
|
||||
index.Full = func(*index.Index) bool { return true }
|
||||
testRebuildIndex(t, nil)
|
||||
}
|
||||
|
||||
// indexErrorBackend modifies the first index after reading.
|
||||
type indexErrorBackend struct {
|
||||
backend.Backend
|
||||
@@ -82,7 +74,7 @@ func (b *indexErrorBackend) Load(ctx context.Context, h backend.Handle, length i
|
||||
// protect hasErred
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
if !b.hasErred && h.Type == restic.IndexFile {
|
||||
if !b.hasErred && h.Type == backend.IndexFile {
|
||||
b.hasErred = true
|
||||
return consumer(errorReadCloser{rd})
|
||||
}
|
||||
@@ -120,6 +112,7 @@ func (b *appendOnlyBackend) Remove(_ context.Context, h backend.Handle) error {
|
||||
}
|
||||
|
||||
func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
|
||||
repository.TestInjectKey(t, restic.TestParseID("b9883c60bed42db51be171ca52f055104b6ea7cfa2bc381c05b2b1f78231280c"), `{"mac":{"k":"maQ4ILA872XnDxHVEno94A==","r":"OptMBABwkgIsMQcHME8cBw=="},"encrypt":"janrR1efN7HyQ8kOZ9zhHixooZ/e+WelH0mT4v9WskQ="}`)
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -52,7 +53,7 @@ func runRepairPacks(ctx context.Context, gopts global.Options, term ui.Terminal,
|
||||
return errors.Fatal("no ids specified")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
@@ -68,9 +69,13 @@ func runRepairPacks(ctx context.Context, gopts global.Options, term ui.Terminal,
|
||||
printer.P("saving backup copies of pack files to current folder")
|
||||
for id := range ids {
|
||||
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
|
||||
// corrupted data is fine
|
||||
if buf == nil {
|
||||
return err
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
// only skip creating a local copy if no data at all could be loaded
|
||||
if err != nil && buf == nil {
|
||||
printer.E("will remove packfile %v due to failed download: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := os.OpenFile("pack-"+id.String(), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
// testRunRepairPacks runs `restic repair packs` with capturing stdout and stderr
|
||||
func testRunRepairPacks(t testing.TB, gopts global.Options, args []string) (string, string, error) {
|
||||
bufStdout, bufStderr, err := withCaptureStdoutStderr(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runRepairPacks(ctx, gopts, gopts.Term, args)
|
||||
})
|
||||
|
||||
return bufStdout.String(), bufStderr.String(), err
|
||||
}
|
||||
|
||||
func TestRunRepairPackfiles(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
// backup of subtree 0/0/9/42
|
||||
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "42")}, BackupOptions{}, env.gopts)
|
||||
testListSnapshots(t, env.gopts, 1)
|
||||
|
||||
packfileID := restic.ID{}
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
rtest.OK(t, repo.LoadIndex(ctx, printer))
|
||||
// load packfiles from master index
|
||||
err = repo.ListBlobs(ctx, func(blob restic.PackBlob) {
|
||||
if blob.Handle().Type == restic.DataBlob {
|
||||
packfileID = blob.PackID()
|
||||
return
|
||||
}
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
rtest.Assert(t, !packfileID.IsNull(), "expected valid packfile ID")
|
||||
packIDString := packfileID.String()
|
||||
filename := filepath.Join(env.gopts.Repo, "data", packIDString[0:2], packIDString)
|
||||
rtest.OK(t, os.Remove(filename))
|
||||
|
||||
_, outError, err := testRunCheckOutput(t, env.gopts, false)
|
||||
rtest.Assert(t, err != nil, "expected check errors, got none")
|
||||
rtest.Assert(t, strings.Contains(string(outError), packIDString), "expected mention of %q", packIDString)
|
||||
|
||||
// change to temporary directory to not pollute the repository with backup files
|
||||
cleanupChdir := rtest.Chdir(t, env.base)
|
||||
defer cleanupChdir()
|
||||
// restic repair packs 'packIDString'
|
||||
_, _, err = testRunRepairPacks(t, env.gopts, []string{packIDString})
|
||||
rtest.OK(t, err)
|
||||
|
||||
// run restic repair snapshots --forget
|
||||
testRunRepairSnapshot(t, env.gopts, true)
|
||||
_, _, err = testRunCheckOutput(t, env.gopts, false)
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -77,8 +78,39 @@ func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) {
|
||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||
}
|
||||
|
||||
// handleUnreadableSnapshotFile is called when FindAll returns an error for ID 'id'
|
||||
func handleUnreadableSnapshotFile(
|
||||
ctx context.Context,
|
||||
be restic.Lister,
|
||||
repo restic.Repository,
|
||||
opts RepairOptions,
|
||||
id string,
|
||||
args []string,
|
||||
printer restic.Printer,
|
||||
) (bool, error) {
|
||||
brokenID, err := restic.Find(ctx, be, restic.SnapshotFile, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if opts.Forget && slices.Index(args, id) >= 0 {
|
||||
if opts.DryRun {
|
||||
printer.P("would remove unreadable snapshot %v", brokenID)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err := repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, brokenID); err != nil {
|
||||
return false, errors.Wrapf(err, "unable to remove unreadable snapshot file %v", brokenID)
|
||||
}
|
||||
printer.P("removed unreadable snapshot %v", brokenID)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, errors.Fatalf("snapshot file %[1]s is unreadable, use `restic repair snapshots --forget %[1]s` to remove it. Original error: %v", brokenID, err)
|
||||
}
|
||||
|
||||
func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOptions, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun, printer)
|
||||
if err != nil {
|
||||
@@ -95,10 +127,11 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
|
||||
return err
|
||||
}
|
||||
|
||||
// Three error cases are checked:
|
||||
// Four error cases are checked:
|
||||
// - tree is a nil tree (-> will be replaced by an empty tree)
|
||||
// - trees which cannot be loaded (-> the tree contents will be removed)
|
||||
// - files whose contents are not fully available (-> file will be modified)
|
||||
// - *checker.SnapshotError
|
||||
rewriter := walker.NewTreeRewriter(walker.RewriteOpts{
|
||||
RewriteNode: func(node *data.Node, path string) *data.Node {
|
||||
if node.Type == data.NodeTypeIrregular || node.Type == data.NodeTypeInvalid {
|
||||
@@ -114,7 +147,7 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
|
||||
var newSize uint64
|
||||
// check all contents and remove if not available
|
||||
for _, id := range node.Content {
|
||||
if size, found := repo.LookupBlobSize(restic.DataBlob, id); !found {
|
||||
if size, found := repo.LookupBlobSize(restic.BlobHandle{Type: restic.DataBlob, ID: id}); !found {
|
||||
ok = false
|
||||
} else {
|
||||
newContent = append(newContent, id)
|
||||
@@ -145,7 +178,15 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
|
||||
})
|
||||
|
||||
changedCount := 0
|
||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) {
|
||||
errOuter := opts.SnapshotFilter.FindAll(ctx, snapshotLister, repo, args, func(id string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
changed, err := handleUnreadableSnapshotFile(ctx, snapshotLister, repo, opts, id, args, printer)
|
||||
if changed {
|
||||
changedCount++
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
printer.P("\n%v", sn)
|
||||
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
|
||||
func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error) {
|
||||
@@ -158,9 +199,11 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
|
||||
if changed {
|
||||
changedCount++
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
return nil
|
||||
})
|
||||
|
||||
if errOuter != nil {
|
||||
return errOuter
|
||||
}
|
||||
|
||||
printer.P("")
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"math/rand"
|
||||
@@ -67,7 +69,7 @@ func TestRepairSnapshotsWithLostData(t *testing.T) {
|
||||
// repository must be ok after removing the broken snapshots
|
||||
testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
|
||||
testListSnapshots(t, env.gopts, 2)
|
||||
_, err := testRunCheckOutput(t, env.gopts, false)
|
||||
_, _, err := testRunCheckOutput(t, env.gopts, false)
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
|
||||
@@ -96,7 +98,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) {
|
||||
testRunRebuildIndex(t, env.gopts)
|
||||
testRunRepairSnapshot(t, env.gopts, true)
|
||||
testListSnapshots(t, env.gopts, 1)
|
||||
_, err := testRunCheckOutput(t, env.gopts, false)
|
||||
_, _, err := testRunCheckOutput(t, env.gopts, false)
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ func TestRepairSnapshotsWithLostRootTree(t *testing.T) {
|
||||
testRunRebuildIndex(t, env.gopts)
|
||||
testRunRepairSnapshot(t, env.gopts, true)
|
||||
testListSnapshots(t, env.gopts, 0)
|
||||
_, err := testRunCheckOutput(t, env.gopts, false)
|
||||
_, _, err := testRunCheckOutput(t, env.gopts, false)
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
|
||||
@@ -136,3 +138,38 @@ func TestRepairSnapshotsIntact(t *testing.T) {
|
||||
rtest.Assert(t, reflect.DeepEqual(oldSnapshotIDs, snapshotIDs), "unexpected snapshot id mismatch %v vs. %v", oldSnapshotIDs, snapshotIDs)
|
||||
testRunCheck(t, env.gopts)
|
||||
}
|
||||
|
||||
func TestRepairSnapshotsBrokenSnapshots(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
// create backup
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, BackupOptions{}, env.gopts)
|
||||
|
||||
// create zero length file in snapshots/
|
||||
// will fail with
|
||||
// failed to load snapshot 1d204771: LoadRaw(<snapshot/1d20477115>): invalid data returned
|
||||
handle, err := os.Create(filepath.Join(env.repo, "snapshots", "1d20477115fb872069a28a80ffb95a82cb8b1b1920de046a68c0195da63f30cf"))
|
||||
rtest.OK(t, err)
|
||||
rtest.OK(t, handle.Close())
|
||||
|
||||
// create some file with a correct sha256 name in snapshots/, will fail with
|
||||
// failed to load snapshot abcd1234: ciphertext verification failed
|
||||
contents := rtest.Random(1234567890123, 42)
|
||||
sha256Contents := sha256.Sum256(contents)
|
||||
target := hex.EncodeToString(sha256Contents[:])
|
||||
rtest.OK(t, os.WriteFile(filepath.Join(env.repo, "snapshots", target), contents, 0o600))
|
||||
|
||||
// run repair snapshots
|
||||
repairOpts := RepairOptions{Forget: true}
|
||||
env.gopts.BackendTestHook = nil
|
||||
_, err = withCaptureStdout(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runRepairSnapshots(ctx, gopts, repairOpts, []string{"1d204771", target[:8]}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
// verify that there are no snapshot errors
|
||||
testRunCheck(t, env.gopts)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/filter"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/restorer"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
restoreui "github.com/restic/restic/internal/ui/restore"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -167,7 +167,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options,
|
||||
return err
|
||||
}
|
||||
|
||||
progress := restoreui.NewProgress(printer, ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
|
||||
progress := restoreui.NewProgress(printer, gopts.Quiet, gopts.JSON, term.CanUpdateStatus())
|
||||
res := restorer.NewRestorer(repo, sn, restorer.Options{
|
||||
DryRun: opts.DryRun,
|
||||
Sparse: opts.Sparse,
|
||||
@@ -281,7 +281,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options,
|
||||
return nil
|
||||
}
|
||||
|
||||
func getXattrSelectFilter(opts RestoreOptions, printer progress.Printer) (func(xattrName string) bool, error) {
|
||||
func getXattrSelectFilter(opts RestoreOptions, printer restic.Printer) (func(xattrName string) bool, error) {
|
||||
hasXattrExcludes := len(opts.ExcludeXattrPattern) > 0
|
||||
hasXattrIncludes := len(opts.IncludeXattrPattern) > 0
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
@@ -242,7 +243,7 @@ func TestRestore(t *testing.T) {
|
||||
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
p := filepath.Join(env.testdata, fmt.Sprintf("foo/bar/testfile%v", i))
|
||||
rtest.OK(t, os.MkdirAll(filepath.Dir(p), 0755))
|
||||
rtest.OK(t, appendRandomData(p, uint(rand.Intn(2<<21))))
|
||||
@@ -329,6 +330,7 @@ func TestRestoreLatest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRestoreWithPermissionFailure(t *testing.T) {
|
||||
repository.TestInjectKey(t, restic.TestParseID("18493b1f93ad90b6bce7ed3afa93395a1f90e981f04145c03e5958cafa2ee33b"), `{"mac":{"k":"7ftgSq7jNM2HiGCyY9TYrg==","r":"o+1bB0wApwqoG7oAXOLyDw=="},"encrypt":"i57gVfyYp9HmXjzE0dSZyrkp2FN9LE75uWFjOuWze1M="}`)
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) {
|
||||
// be updated accordingly.
|
||||
type rewriteFilterFunc func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error)
|
||||
|
||||
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.Snapshot, opts RewriteOptions, printer progress.Printer) (bool, error) {
|
||||
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.Snapshot, opts RewriteOptions, printer restic.Printer) (bool, error) {
|
||||
if sn.Tree == nil {
|
||||
return false, errors.Errorf("snapshot %v has nil tree", sn.ID().Str())
|
||||
}
|
||||
@@ -191,7 +191,7 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.
|
||||
}
|
||||
|
||||
func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *data.Snapshot,
|
||||
filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string, printer progress.Printer,
|
||||
filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string, printer restic.Printer,
|
||||
keepEmptySnapshot bool) (bool, error) {
|
||||
|
||||
var filteredTree restic.ID
|
||||
@@ -298,7 +298,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
|
||||
return errors.Fatal("exclude and include patterns are mutually exclusive")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
var (
|
||||
repo *repository.Repository
|
||||
@@ -327,7 +327,10 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
|
||||
}
|
||||
|
||||
changedCount := 0
|
||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) {
|
||||
err = opts.SnapshotFilter.FindAll(ctx, snapshotLister, repo, args, func(_ string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printer.P("\n%v", sn)
|
||||
changed, err := rewriteSnapshot(ctx, repo, sn, opts, printer)
|
||||
if err != nil {
|
||||
@@ -336,9 +339,10 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
|
||||
if changed {
|
||||
changedCount++
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.P("")
|
||||
@@ -359,7 +363,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer progress.Printer) (rewriteNode walker.NodeRewriteFunc, keepEmptyDirectory walker.NodeKeepEmptyDirectoryFunc) {
|
||||
func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer restic.Printer) (rewriteNode walker.NodeRewriteFunc, keepEmptyDirectory walker.NodeKeepEmptyDirectoryFunc) {
|
||||
inSelectByName := func(nodepath string, node *data.Node) bool {
|
||||
for _, include := range includeByNameFuncs {
|
||||
matched, childMayMatch := include(nodepath)
|
||||
@@ -406,7 +410,7 @@ func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer
|
||||
return rewriteNode, keepEmptyDirectory
|
||||
}
|
||||
|
||||
func gatherExcludeFilters(excludeByNameFuncs []filter.RejectByNameFunc, printer progress.Printer) (rewriteNode walker.NodeRewriteFunc) {
|
||||
func gatherExcludeFilters(excludeByNameFuncs []filter.RejectByNameFunc, printer restic.Printer) (rewriteNode walker.NodeRewriteFunc) {
|
||||
exSelectByName := func(nodepath string) bool {
|
||||
for _, reject := range excludeByNameFuncs {
|
||||
if reject(nodepath) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func testRunRewriteExclude(t testing.TB, gopts global.Options, excludes []string, forget bool, metadata snapshotMetadataArgs) {
|
||||
@@ -42,7 +43,7 @@ func testLsOutputContainsCount(t testing.TB, gopts global.Options, lsOpts LsOpti
|
||||
t.Helper()
|
||||
out := testRunLsWithOpts(t, gopts, lsOpts, lsArgs)
|
||||
count := 0
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
for line := range strings.SplitSeq(string(out), "\n") {
|
||||
if strings.Contains(line, substring) {
|
||||
count++
|
||||
}
|
||||
@@ -81,7 +82,7 @@ func getSnapshot(t testing.TB, snapshotID restic.ID, env *testEnvironment) *data
|
||||
|
||||
var snapshots []*data.Snapshot
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
@@ -157,7 +158,7 @@ func testRewriteMetadata(t *testing.T, metadata snapshotMetadataArgs) {
|
||||
|
||||
var snapshots []*data.Snapshot
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
@@ -205,7 +206,7 @@ func TestRewriteSnaphotSummary(t *testing.T) {
|
||||
// replace snapshot by one without a summary
|
||||
var oldSummary *data.SnapshotSummary
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/selfupdate"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -84,7 +85,7 @@ func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts global.Opt
|
||||
}
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||
printer.P("writing restic to %v", opts.Output)
|
||||
|
||||
v, err := selfupdate.DownloadLatestStableRelease(ctx, opts.Output, global.Version, printer.P)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -81,7 +82,7 @@ func (opts *SnapshotOptions) Finalize() error {
|
||||
}
|
||||
|
||||
func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -89,12 +90,17 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Option
|
||||
defer unlock()
|
||||
|
||||
var snapshots data.Snapshots
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
|
||||
err = opts.SnapshotFilter.FindAll(ctx, repo, repo, args, func(_ string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshots = append(snapshots, sn)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
snapshotGroups, grouped, err := data.GroupSnapshots(snapshots, opts.GroupBy)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+35
-109
@@ -7,18 +7,15 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/restic/chunker"
|
||||
"github.com/restic/restic/internal/crypto"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/restorer"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
statsui "github.com/restic/restic/internal/ui/stats"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
|
||||
@@ -34,13 +31,13 @@ func newStatsCommand(globalOptions *global.Options) *cobra.Command {
|
||||
Short: "Scan the repository and show basic statistics",
|
||||
Long: `
|
||||
The "stats" command walks one or multiple snapshots in a repository
|
||||
and accumulates statistics about the data stored therein. It reports
|
||||
and accumulates statistics about the data stored therein. It reports
|
||||
on the number of unique files and their sizes, according to one of
|
||||
the counting modes as given by the --mode flag.
|
||||
|
||||
It operates on all snapshots matching the selection criteria or all
|
||||
snapshots if nothing is specified. The special snapshot ID "latest"
|
||||
is also supported. Some modes make more sense over
|
||||
is also supported. Some modes make more sense over
|
||||
just a single snapshot, while others are useful across all snapshots,
|
||||
depending on what you are trying to calculate.
|
||||
|
||||
@@ -104,7 +101,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
|
||||
return err
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
@@ -137,17 +134,19 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
|
||||
}
|
||||
|
||||
var snapshots data.Snapshots
|
||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) {
|
||||
err = opts.SnapshotFilter.FindAll(ctx, snapshotLister, repo, args, func(_ string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshots = append(snapshots, sn)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statsProgress := newStatsProgress(term, !gopts.JSON, uint64(len(snapshots)))
|
||||
|
||||
updater := progress.NewUpdater(ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()), func(runtime time.Duration, final bool) {
|
||||
statsProgress.printProgress(runtime, final)
|
||||
})
|
||||
|
||||
defer updater.Done()
|
||||
statsProgress := statsui.NewProgress(term, gopts.Quiet, gopts.JSON, uint64(len(snapshots)))
|
||||
defer statsProgress.Done()
|
||||
|
||||
for _, sn := range snapshots {
|
||||
err = statsWalkSnapshot(ctx, sn, repo, opts, stats, statsProgress)
|
||||
@@ -162,20 +161,20 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
|
||||
if opts.countMode == countModeRawData {
|
||||
// the blob handles have been collected, but not yet counted
|
||||
for blobHandle := range stats.blobs.Keys() {
|
||||
pbs := repo.LookupBlob(blobHandle.Type, blobHandle.ID)
|
||||
pbs := repo.LookupBlob(blobHandle)
|
||||
if len(pbs) == 0 {
|
||||
return fmt.Errorf("blob %v not found", blobHandle)
|
||||
}
|
||||
stats.TotalSize += uint64(pbs[0].Length)
|
||||
stats.TotalSize += uint64(pbs[0].CiphertextLength())
|
||||
if repo.Config().Version >= 2 {
|
||||
stats.TotalUncompressedSize += uint64(crypto.CiphertextLength(int(pbs[0].DataLength())))
|
||||
stats.TotalUncompressedSize += uint64(pbs[0].UncompressedCiphertextLength())
|
||||
if pbs[0].IsCompressed() {
|
||||
stats.TotalCompressedBlobsSize += uint64(pbs[0].Length)
|
||||
stats.TotalCompressedBlobsUncompressedSize += uint64(crypto.CiphertextLength(int(pbs[0].DataLength())))
|
||||
stats.TotalCompressedBlobsSize += uint64(pbs[0].CiphertextLength())
|
||||
stats.TotalCompressedBlobsUncompressedSize += uint64(pbs[0].UncompressedCiphertextLength())
|
||||
}
|
||||
}
|
||||
stats.TotalBlobCount++
|
||||
statsProgress.update(0, 1, uint64(pbs[0].Length))
|
||||
statsProgress.Update(0, 1, uint64(pbs[0].CiphertextLength()))
|
||||
}
|
||||
if stats.TotalCompressedBlobsSize > 0 {
|
||||
stats.CompressionRatio = float64(stats.TotalCompressedBlobsUncompressedSize) / float64(stats.TotalCompressedBlobsSize)
|
||||
@@ -186,7 +185,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
|
||||
}
|
||||
}
|
||||
// stop progress bar to prevent mangled output
|
||||
updater.Done()
|
||||
statsProgress.Done()
|
||||
|
||||
if gopts.JSON {
|
||||
err = json.NewEncoder(gopts.Term.OutputWriter()).Encode(stats)
|
||||
@@ -221,8 +220,8 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
|
||||
return nil
|
||||
}
|
||||
|
||||
func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic.Loader, opts StatsOptions, stats *statsContainer, progress *statsProgress) error {
|
||||
progress.processSnapshot()
|
||||
func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic.Loader, opts StatsOptions, stats *statsContainer, sp *statsui.Progress) error {
|
||||
sp.ProcessSnapshot()
|
||||
if snapshot.Tree == nil {
|
||||
return fmt.Errorf("snapshot %s has nil tree", snapshot.ID().Str())
|
||||
}
|
||||
@@ -232,12 +231,12 @@ func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic
|
||||
if opts.countMode == countModeRawData {
|
||||
// count just the sizes of unique blobs; we don't need to walk the tree
|
||||
// ourselves in this case, since a nifty function does it for us
|
||||
return data.FindUsedBlobs(ctx, repo, restic.IDs{*snapshot.Tree}, stats.blobs, nil)
|
||||
return data.FindUsedBlobs(ctx, repo, restic.IDs{*snapshot.Tree}, stats.blobs, restic.NoopCounter)
|
||||
}
|
||||
|
||||
hardLinkIndex := restorer.NewHardlinkIndex[struct{}]()
|
||||
hardLinkIndex := data.NewHardlinkIndex[struct{}]()
|
||||
err := walker.Walk(ctx, repo, *snapshot.Tree, walker.WalkVisitor{
|
||||
ProcessNode: statsWalkTree(repo, opts, stats, hardLinkIndex, progress),
|
||||
ProcessNode: statsWalkTree(repo, opts, stats, hardLinkIndex, sp),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walking tree %s: %v", *snapshot.Tree, err)
|
||||
@@ -246,7 +245,7 @@ func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic
|
||||
return nil
|
||||
}
|
||||
|
||||
func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer, hardLinkIndex *restorer.HardlinkIndex[struct{}], progress *statsProgress) walker.WalkFunc {
|
||||
func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer, hardLinkIndex *data.HardlinkIndex[struct{}], progress *statsui.Progress) walker.WalkFunc {
|
||||
return func(parentTreeID restic.ID, npath string, node *data.Node, nodeErr error) error {
|
||||
if nodeErr != nil {
|
||||
return nodeErr
|
||||
@@ -254,7 +253,7 @@ func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer,
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
progress.update(1, 0, uint64(node.Size))
|
||||
progress.Update(1, 0, uint64(node.Size))
|
||||
if opts.countMode == countModeUniqueFilesByContents || opts.countMode == countModeBlobsPerFile {
|
||||
// only count this file if we haven't visited it before
|
||||
fid := makeFileIDByContents(node)
|
||||
@@ -274,14 +273,14 @@ func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer,
|
||||
// ensure we have this file (by path) in our map; in this
|
||||
// mode, a file is unique by both contents and path
|
||||
nodePath := filepath.Join(npath, node.Name)
|
||||
progress.update(0, 1, 0)
|
||||
progress.Update(0, 1, 0)
|
||||
if _, ok := stats.fileBlobs[nodePath]; !ok {
|
||||
stats.fileBlobs[nodePath] = restic.NewIDSet()
|
||||
stats.TotalFileCount++
|
||||
}
|
||||
if _, ok := stats.fileBlobs[nodePath][blobID]; !ok {
|
||||
// is always a data blob since we're accessing it via a file's Content array
|
||||
blobSize, found := repo.LookupBlobSize(restic.DataBlob, blobID)
|
||||
blobSize, found := repo.LookupBlobSize(restic.BlobHandle{Type: restic.DataBlob, ID: blobID})
|
||||
if !found {
|
||||
return fmt.Errorf("blob %s not found for tree %s", blobID, parentTreeID)
|
||||
}
|
||||
@@ -371,76 +370,6 @@ type statsContainer struct {
|
||||
// independent of references to files
|
||||
blobs restic.AssociatedBlobSet
|
||||
}
|
||||
type statsProgress struct {
|
||||
term ui.Terminal
|
||||
m sync.Mutex
|
||||
snapshotCount uint64
|
||||
show bool
|
||||
|
||||
processedSnapshotCount uint64
|
||||
processedFileCount uint64
|
||||
processedBlobCount uint64
|
||||
processedSize uint64
|
||||
}
|
||||
|
||||
func newStatsProgress(term ui.Terminal, show bool, snapshotCount uint64) *statsProgress {
|
||||
return &statsProgress{
|
||||
term: term,
|
||||
show: show,
|
||||
snapshotCount: snapshotCount,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statsProgress) printProgress(runtime time.Duration, final bool) {
|
||||
if !s.show {
|
||||
return
|
||||
}
|
||||
s.m.Lock()
|
||||
|
||||
progressBase := s.processedSnapshotCount
|
||||
if progressBase > 0 && !final {
|
||||
progressBase--
|
||||
}
|
||||
|
||||
status := fmt.Sprintf("[%s] %s %d / %d snapshots", ui.FormatDuration(runtime), ui.FormatPercent(progressBase, s.snapshotCount), s.processedSnapshotCount, s.snapshotCount)
|
||||
|
||||
if s.processedFileCount > 0 {
|
||||
status += fmt.Sprintf(", %v files", s.processedFileCount)
|
||||
}
|
||||
|
||||
if s.processedBlobCount > 0 {
|
||||
status += fmt.Sprintf(", %d blobs", s.processedBlobCount)
|
||||
}
|
||||
|
||||
status += fmt.Sprintf(", %s", ui.FormatBytes(s.processedSize))
|
||||
s.m.Unlock()
|
||||
|
||||
if final {
|
||||
s.term.SetStatus(nil)
|
||||
s.term.Print(status)
|
||||
} else {
|
||||
s.term.SetStatus([]string{status})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statsProgress) update(fileCount uint64, blobCount uint64, size uint64) {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
|
||||
s.processedFileCount += fileCount
|
||||
s.processedBlobCount += blobCount
|
||||
s.processedSize += size
|
||||
}
|
||||
|
||||
func (s *statsProgress) processSnapshot() {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
|
||||
s.processedSnapshotCount++
|
||||
s.processedFileCount = 0
|
||||
s.processedBlobCount = 0
|
||||
s.processedSize = 0
|
||||
}
|
||||
|
||||
// fileID is a 256-bit hash that distinguishes unique files.
|
||||
type fileID [32]byte
|
||||
@@ -453,7 +382,7 @@ const (
|
||||
countModeDebug = "debug"
|
||||
)
|
||||
|
||||
func statsDebug(ctx context.Context, repo restic.Repository, printer progress.Printer) error {
|
||||
func statsDebug(ctx context.Context, repo restic.Repository, printer restic.Printer) error {
|
||||
printer.E("Collecting size statistics\n\n")
|
||||
for _, t := range []restic.FileType{restic.KeyFile, restic.LockFile, restic.IndexFile, restic.SnapshotFile, restic.PackFile} {
|
||||
hist, err := statsDebugFileType(ctx, repo, t)
|
||||
@@ -486,12 +415,12 @@ func statsDebugFileType(ctx context.Context, repo restic.Lister, tpe restic.File
|
||||
|
||||
func statsDebugBlobs(ctx context.Context, repo restic.Repository) ([restic.NumBlobTypes]*sizeHistogram, error) {
|
||||
var hist [restic.NumBlobTypes]*sizeHistogram
|
||||
for i := 0; i < len(hist); i++ {
|
||||
for i := range len(hist) {
|
||||
hist[i] = newSizeHistogram(2 * chunker.MaxSize)
|
||||
}
|
||||
|
||||
err := repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
||||
hist[pb.Type].Add(uint64(pb.Length))
|
||||
err := repo.ListBlobs(ctx, func(pb restic.PackBlob) {
|
||||
hist[pb.Handle().Type].Add(uint64(pb.CiphertextLength()))
|
||||
})
|
||||
|
||||
return hist, err
|
||||
@@ -517,10 +446,7 @@ func newSizeHistogram(sizeLimit uint64) *sizeHistogram {
|
||||
growthFactor := uint64(10)
|
||||
|
||||
for lowerBound < sizeLimit {
|
||||
upperBound := lowerBound*growthFactor - 1
|
||||
if upperBound > sizeLimit {
|
||||
upperBound = sizeLimit
|
||||
}
|
||||
upperBound := min(lowerBound*growthFactor-1, sizeLimit)
|
||||
h.buckets = append(h.buckets, sizeClass{lowerBound, upperBound, 0})
|
||||
lowerBound *= growthFactor
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
)
|
||||
|
||||
func TestSizeHistogramNew(t *testing.T) {
|
||||
@@ -26,7 +24,7 @@ func TestSizeHistogramNew(t *testing.T) {
|
||||
|
||||
func TestSizeHistogramAdd(t *testing.T) {
|
||||
h := newSizeHistogram(42)
|
||||
for i := uint64(0); i < 45; i++ {
|
||||
for i := range uint64(45) {
|
||||
h.Add(i)
|
||||
}
|
||||
|
||||
@@ -62,38 +60,3 @@ func TestSizeHistogramString(t *testing.T) {
|
||||
rtest.Equals(t, "Count: 3\nTotal Size: 11 B\nSize Count\n-------------------\n 0 - 0 Byte 1\n 1 - 9 Byte 1\n10 - 42 Byte 1\n-------------------\n", h.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestStatsProgress(t *testing.T) {
|
||||
term := &ui.MockTerminal{}
|
||||
|
||||
progress := newStatsProgress(term, true, 2)
|
||||
progress.printProgress(0*time.Second, false)
|
||||
rtest.Equals(t, []string{"[0:00] 0.00% 0 / 2 snapshots, 0 B"}, term.Output)
|
||||
|
||||
progress.processSnapshot()
|
||||
progress.update(1, 2, 3)
|
||||
progress.printProgress(5*time.Second, false)
|
||||
// Output differs from the previous one because the progress is based on the number of processed snapshots,
|
||||
// 1/2 snapshots means processing the snapshot 1 currently
|
||||
rtest.Equals(t, []string{"[0:05] 0.00% 1 / 2 snapshots, 1 files, 2 blobs, 3 B"}, term.Output)
|
||||
|
||||
progress.processSnapshot()
|
||||
progress.printProgress(10*time.Second, false)
|
||||
rtest.Equals(t, []string{"[0:10] 50.00% 2 / 2 snapshots, 0 B"}, term.Output)
|
||||
|
||||
progress.update(4, 5, 6)
|
||||
progress.printProgress(15*time.Second, false)
|
||||
rtest.Equals(t, []string{"[0:15] 50.00% 2 / 2 snapshots, 4 files, 5 blobs, 6 B"}, term.Output)
|
||||
|
||||
progress.printProgress(20*time.Second, true)
|
||||
rtest.Equals(t, []string{"[0:20] 100.00% 2 / 2 snapshots, 4 files, 5 blobs, 6 B"}, term.Output)
|
||||
}
|
||||
|
||||
func TestStatsProgressJSON(t *testing.T) {
|
||||
term := &ui.MockTerminal{}
|
||||
|
||||
progress := newStatsProgress(term, false, 2)
|
||||
progress.printProgress(0*time.Second, false)
|
||||
// JSON output is not available yet, so just make sure to not break normal json output
|
||||
rtest.Equals(t, nil, term.Output)
|
||||
}
|
||||
|
||||
+11
-7
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func newTagCommand(globalOptions *global.Options) *cobra.Command {
|
||||
@@ -120,7 +121,7 @@ func changeTags(ctx context.Context, repo *repository.Repository, sn *data.Snaps
|
||||
}
|
||||
|
||||
func runTag(ctx context.Context, opts TagOptions, gopts global.Options, term ui.Terminal, args []string) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 {
|
||||
return errors.Fatal("nothing to do!")
|
||||
@@ -158,19 +159,22 @@ func runTag(ctx context.Context, opts TagOptions, gopts global.Options, term ui.
|
||||
}
|
||||
}
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
|
||||
err = opts.SnapshotFilter.FindAll(ctx, repo, repo, args, func(_ string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changed, err := changeTags(ctx, repo, sn, opts.SetTags.Flatten(), opts.AddTags.Flatten(), opts.RemoveTags.Flatten(), printFunc)
|
||||
if err != nil {
|
||||
printer.E("unable to modify the tags for snapshot ID %q, ignoring: %v", sn.ID(), err)
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
if changed {
|
||||
summary.ChangedSnapshots++
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printSummary(summary)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -47,7 +48,7 @@ func (opts *UnlockOptions) AddFlags(f *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
func runUnlock(ctx context.Context, opts UnlockOptions, gopts global.Options, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
repo, err := global.OpenRepository(ctx, gopts, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ Exit status is 1 if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
printer := ui.NewProgressPrinter(globalOptions.JSON, globalOptions.Verbosity, globalOptions.Term)
|
||||
printer := progress.NewTerminalPrinter(globalOptions.JSON, globalOptions.Verbosity, globalOptions.Term)
|
||||
|
||||
if globalOptions.JSON {
|
||||
type jsonVersion struct {
|
||||
|
||||
+1
-34
@@ -1,17 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// initMultiSnapshotFilter is used for commands that work on multiple snapshots
|
||||
// MUST be combined with FindFilteredSnapshots
|
||||
// MUST be combined with (*data,SnapshotFilter).FindAll
|
||||
// MUST be followed by finalizeSnapshotFilter after flag parsing
|
||||
func initMultiSnapshotFilter(flags *pflag.FlagSet, filt *data.SnapshotFilter, addHostShorthand bool) {
|
||||
hostShorthand := "H"
|
||||
@@ -47,33 +44,3 @@ func finalizeSnapshotFilter(filt *data.SnapshotFilter) {
|
||||
filt.Hosts = nil
|
||||
}
|
||||
}
|
||||
|
||||
// FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots.
|
||||
func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, f *data.SnapshotFilter, snapshotIDs []string, printer progress.Printer) <-chan *data.Snapshot {
|
||||
out := make(chan *data.Snapshot)
|
||||
go func() {
|
||||
defer close(out)
|
||||
be, err := restic.MemorizeList(ctx, be, restic.SnapshotFile)
|
||||
if err != nil {
|
||||
printer.E("could not load snapshots: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = f.FindAll(ctx, be, loader, snapshotIDs, func(id string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
printer.E("Ignoring %q: %v", id, err)
|
||||
} else {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case out <- sn:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
printer.E("could not load snapshots: %v", err)
|
||||
}
|
||||
}()
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"github.com/restic/restic/internal/ui"
|
||||
)
|
||||
|
||||
func formatNode(path string, n *data.Node, long bool, human bool) string {
|
||||
func formatNodeOutput(n lsNodeOutput, long bool, human bool) string {
|
||||
if !long {
|
||||
return path
|
||||
return n.Path
|
||||
}
|
||||
|
||||
var mode os.FileMode
|
||||
@@ -19,9 +19,9 @@ func formatNode(path string, n *data.Node, long bool, human bool) string {
|
||||
|
||||
var size string
|
||||
if human {
|
||||
size = ui.FormatBytes(n.Size)
|
||||
size = ui.FormatBytes(n.fileSize())
|
||||
} else {
|
||||
size = fmt.Sprintf("%6d", n.Size)
|
||||
size = fmt.Sprintf("%6d", n.fileSize())
|
||||
}
|
||||
|
||||
switch n.Type {
|
||||
@@ -44,6 +44,10 @@ func formatNode(path string, n *data.Node, long bool, human bool) string {
|
||||
|
||||
return fmt.Sprintf("%s %5d %5d %s %s %s%s",
|
||||
mode|n.Mode, n.UID, n.GID, size,
|
||||
n.ModTime.Local().Format(global.TimeFormat), path,
|
||||
n.ModTime.Local().Format(global.TimeFormat), n.Path,
|
||||
target)
|
||||
}
|
||||
|
||||
func formatNode(path string, n *data.Node, long bool, human bool) string {
|
||||
return formatNodeOutput(lsNodeOutputFrom(path, n), long, human)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/backend"
|
||||
"github.com/restic/restic/internal/backend/all"
|
||||
"github.com/restic/restic/internal/backend/layout"
|
||||
"github.com/restic/restic/internal/backend/retry"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
@@ -193,6 +194,7 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
|
||||
repository.TestUseLowSecurityKDFParameters(t)
|
||||
restic.TestDisableCheckPolynomial(t)
|
||||
retry.TestFastRetries(t)
|
||||
layout.TestDisablePackSubdirs(t)
|
||||
|
||||
tempdir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-")
|
||||
rtest.OK(t, err)
|
||||
@@ -211,11 +213,12 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
|
||||
rtest.OK(t, os.MkdirAll(env.repo, 0700))
|
||||
|
||||
env.gopts = global.Options{
|
||||
Repo: env.repo,
|
||||
Quiet: true,
|
||||
CacheDir: env.cache,
|
||||
Password: rtest.TestPassword,
|
||||
Extended: make(options.Options),
|
||||
Repo: env.repo,
|
||||
Quiet: true,
|
||||
CacheDir: env.cache,
|
||||
Password: rtest.TestPassword,
|
||||
Extended: make(options.Options),
|
||||
Compression: repository.CompressionFastest,
|
||||
|
||||
// replace this hook with "nil" if listing a filetype more than once is necessary
|
||||
BackendTestHook: func(r backend.Backend) (backend.Backend, error) { return newOrderedListOnceBackend(r), nil },
|
||||
@@ -244,7 +247,7 @@ func testSetupBackupData(t testing.TB, env *testEnvironment) string {
|
||||
func listPacks(gopts global.Options, t *testing.T) restic.IDSet {
|
||||
var packs restic.IDSet
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
@@ -263,16 +266,16 @@ func listPacks(gopts global.Options, t *testing.T) restic.IDSet {
|
||||
func listTreePacks(gopts global.Options, t *testing.T) restic.IDSet {
|
||||
var treePacks restic.IDSet
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
rtest.OK(t, r.LoadIndex(ctx, nil))
|
||||
rtest.OK(t, r.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
|
||||
treePacks = restic.NewIDSet()
|
||||
return r.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
||||
if pb.Type == restic.TreeBlob {
|
||||
treePacks.Insert(pb.PackID)
|
||||
return r.ListBlobs(ctx, func(pb restic.PackBlob) {
|
||||
if pb.Handle().Type == restic.TreeBlob {
|
||||
treePacks.Insert(pb.PackID())
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -294,13 +297,13 @@ func captureBackend(gopts *global.Options) func() backend.Backend {
|
||||
func removePacks(gopts global.Options, t testing.TB, remove restic.IDSet) {
|
||||
be := captureBackend(&gopts)
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
ctx, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
for id := range remove {
|
||||
rtest.OK(t, be().Remove(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()}))
|
||||
rtest.OK(t, be().Remove(ctx, backend.Handle{Type: backend.PackFile, Name: id.String()}))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -310,18 +313,18 @@ func removePacks(gopts global.Options, t testing.TB, remove restic.IDSet) {
|
||||
func removePacksExcept(gopts global.Options, t testing.TB, keep restic.IDSet, removeTreePacks bool) {
|
||||
be := captureBackend(&gopts)
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
ctx, r, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
// Get all tree packs
|
||||
rtest.OK(t, r.LoadIndex(ctx, nil))
|
||||
rtest.OK(t, r.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
|
||||
|
||||
treePacks := restic.NewIDSet()
|
||||
rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
||||
if pb.Type == restic.TreeBlob {
|
||||
treePacks.Insert(pb.PackID)
|
||||
rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackBlob) {
|
||||
if pb.Handle().Type == restic.TreeBlob {
|
||||
treePacks.Insert(pb.PackID())
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -330,20 +333,14 @@ func removePacksExcept(gopts global.Options, t testing.TB, keep restic.IDSet, re
|
||||
if treePacks.Has(id) != removeTreePacks || keep.Has(id) {
|
||||
return nil
|
||||
}
|
||||
return be().Remove(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()})
|
||||
return be().Remove(ctx, backend.Handle{Type: backend.PackFile, Name: id.String()})
|
||||
})
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
|
||||
func includes(haystack []string, needle string) bool {
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return slices.Contains(haystack, needle)
|
||||
}
|
||||
|
||||
func loadSnapshotMap(t testing.TB, gopts global.Options) map[string]struct{} {
|
||||
@@ -371,7 +368,7 @@ func lastSnapshot(old, new map[string]struct{}) (map[string]struct{}, string) {
|
||||
func testLoadSnapshot(t testing.TB, gopts global.Options, id restic.ID) *data.Snapshot {
|
||||
var snapshot *data.Snapshot
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
@@ -421,6 +418,13 @@ func withCaptureStdout(t testing.TB, gopts global.Options, callback func(ctx con
|
||||
return buf, err
|
||||
}
|
||||
|
||||
func withCaptureStdoutStderr(t testing.TB, gopts global.Options, callback func(ctx context.Context, gopts global.Options) error) (*bytes.Buffer, *bytes.Buffer, error) {
|
||||
bufStdout := bytes.NewBuffer(nil)
|
||||
bufStderr := bytes.NewBuffer(nil)
|
||||
err := withTermStatusRaw(os.Stdin, bufStdout, bufStderr, gopts, callback)
|
||||
return bufStdout, bufStderr, err
|
||||
}
|
||||
|
||||
func withTermStatus(t testing.TB, gopts global.Options, callback func(ctx context.Context, gopts global.Options) error) error {
|
||||
// stdout and stderr are written to by printer functions etc. That is the written data
|
||||
// usually consists of one or multiple lines and therefore can be handled well
|
||||
@@ -429,18 +433,10 @@ func withTermStatus(t testing.TB, gopts global.Options, callback func(ctx contex
|
||||
}
|
||||
|
||||
func withTermStatusRaw(stdin io.ReadCloser, stdout, stderr io.Writer, gopts global.Options, callback func(ctx context.Context, gopts global.Options) error) error {
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
var wg sync.WaitGroup
|
||||
|
||||
term := termstatus.New(stdin, stdout, stderr, gopts.Quiet)
|
||||
term, cancel := termstatus.Setup(stdin, stdout, stderr, gopts.Quiet)
|
||||
gopts.Term = term
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
term.Run(ctx)
|
||||
}()
|
||||
|
||||
defer wg.Wait()
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
|
||||
return callback(ctx, gopts)
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func TestCheckRestoreNoLock(t *testing.T) {
|
||||
repository.TestInjectKey(t, restic.TestParseID("a19acdab068765b022ffb81cb5aac83c5de4bf4fbce0d26e9ade8e636c6ae49f"), `{"mac":{"k":"TbkpCBdNYAvAwb+64r8VGw==","r":"Q5V1CnAvBQREgJAOQD40Bw=="},"encrypt":"SjCkTpms+XOUJR5LSsy2G+uO9ngG7H0L+IVwPV4u70A="}`)
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -46,14 +48,14 @@ func TestCheckRestoreNoLock(t *testing.T) {
|
||||
// is expected by the first listing + some operations.
|
||||
type listOnceBackend struct {
|
||||
backend.Backend
|
||||
listedFileType map[restic.FileType]bool
|
||||
listedFileType map[backend.FileType]bool
|
||||
strictOrder bool
|
||||
}
|
||||
|
||||
func newListOnceBackend(be backend.Backend) *listOnceBackend {
|
||||
return &listOnceBackend{
|
||||
Backend: be,
|
||||
listedFileType: make(map[restic.FileType]bool),
|
||||
listedFileType: make(map[backend.FileType]bool),
|
||||
strictOrder: false,
|
||||
}
|
||||
}
|
||||
@@ -61,16 +63,16 @@ func newListOnceBackend(be backend.Backend) *listOnceBackend {
|
||||
func newOrderedListOnceBackend(be backend.Backend) *listOnceBackend {
|
||||
return &listOnceBackend{
|
||||
Backend: be,
|
||||
listedFileType: make(map[restic.FileType]bool),
|
||||
listedFileType: make(map[backend.FileType]bool),
|
||||
strictOrder: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (be *listOnceBackend) List(ctx context.Context, t restic.FileType, fn func(backend.FileInfo) error) error {
|
||||
if t != restic.LockFile && be.listedFileType[t] {
|
||||
func (be *listOnceBackend) List(ctx context.Context, t backend.FileType, fn func(backend.FileInfo) error) error {
|
||||
if t != backend.LockFile && be.listedFileType[t] {
|
||||
return errors.Errorf("tried listing type %v the second time", t)
|
||||
}
|
||||
if be.strictOrder && t == restic.SnapshotFile && be.listedFileType[restic.IndexFile] {
|
||||
if be.strictOrder && t == backend.SnapshotFile && be.listedFileType[backend.IndexFile] {
|
||||
return errors.Errorf("tried listing type snapshots after index")
|
||||
}
|
||||
be.listedFileType[t] = true
|
||||
@@ -165,20 +167,32 @@ func TestFindListOnce(t *testing.T) {
|
||||
|
||||
var snapshotIDs restic.IDSet
|
||||
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
printer := progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
snapshotIDs = restic.NewIDSet()
|
||||
// specify the two oldest snapshots explicitly and use "latest" to reference the newest one
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &data.SnapshotFilter{}, []string{
|
||||
secondSnapshot[0].String(),
|
||||
secondSnapshot[1].String()[:8],
|
||||
"latest",
|
||||
}, printer) {
|
||||
snapshotIDs.Insert(*sn.ID())
|
||||
err = (&data.SnapshotFilter{}).FindAll(ctx, repo, repo,
|
||||
[]string{
|
||||
secondSnapshot[0].String(),
|
||||
secondSnapshot[1].String()[:8],
|
||||
"latest",
|
||||
},
|
||||
|
||||
func(id string, sn *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshotIDs.Insert(*sn.ID())
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
@@ -194,7 +208,7 @@ type failConfigOnceBackend struct {
|
||||
func (be *failConfigOnceBackend) Load(ctx context.Context, h backend.Handle,
|
||||
length int, offset int64, fn func(rd io.Reader) error) error {
|
||||
|
||||
if !be.failedOnce && h.Type == restic.ConfigFile {
|
||||
if !be.failedOnce && h.Type == backend.ConfigFile {
|
||||
be.failedOnce = true
|
||||
return fmt.Errorf("oops")
|
||||
}
|
||||
@@ -202,7 +216,7 @@ func (be *failConfigOnceBackend) Load(ctx context.Context, h backend.Handle,
|
||||
}
|
||||
|
||||
func (be *failConfigOnceBackend) Stat(ctx context.Context, h backend.Handle) (backend.FileInfo, error) {
|
||||
if !be.failedOnce && h.Type == restic.ConfigFile {
|
||||
if !be.failedOnce && h.Type == backend.ConfigFile {
|
||||
be.failedOnce = true
|
||||
return backend.FileInfo{}, fmt.Errorf("oops")
|
||||
}
|
||||
|
||||
+6
-10
@@ -5,10 +5,10 @@ import (
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bool, exclusive bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) {
|
||||
func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bool, exclusive bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) {
|
||||
repo, err := global.OpenRepository(ctx, gopts, printer)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -16,9 +16,7 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
|
||||
|
||||
unlock := func() {}
|
||||
if !dryRun {
|
||||
var lock *repository.Unlocker
|
||||
|
||||
lock, ctx, err = repository.Lock(ctx, repo, exclusive, gopts.RetryLock, func(msg string) {
|
||||
unlock, ctx, err = repository.LockRepo(ctx, repo, exclusive, gopts.RetryLock, func(msg string) {
|
||||
if !gopts.JSON {
|
||||
printer.P("%s", msg)
|
||||
}
|
||||
@@ -26,8 +24,6 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
unlock = lock.Unlock
|
||||
} else {
|
||||
repo.SetDryRun()
|
||||
}
|
||||
@@ -35,18 +31,18 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
|
||||
return ctx, repo, unlock, nil
|
||||
}
|
||||
|
||||
func openWithReadLock(ctx context.Context, gopts global.Options, noLock bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) {
|
||||
func openWithReadLock(ctx context.Context, gopts global.Options, noLock bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) {
|
||||
// TODO enforce read-only operations once the locking code has moved to the repository
|
||||
// As in-depth hardening, put the repository into read-only mode if noLock is true
|
||||
// Not possible if the repository has to be locked.
|
||||
return internalOpenWithLocked(ctx, gopts, noLock, false, printer)
|
||||
}
|
||||
|
||||
func openWithAppendLock(ctx context.Context, gopts global.Options, dryRun bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) {
|
||||
func openWithAppendLock(ctx context.Context, gopts global.Options, dryRun bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) {
|
||||
// TODO enforce non-exclusive operations once the locking code has moved to the repository
|
||||
return internalOpenWithLocked(ctx, gopts, dryRun, false, printer)
|
||||
}
|
||||
|
||||
func openWithExclusiveLock(ctx context.Context, gopts global.Options, dryRun bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) {
|
||||
func openWithExclusiveLock(ctx context.Context, gopts global.Options, dryRun bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) {
|
||||
return internalOpenWithLocked(ctx, gopts, dryRun, true, printer)
|
||||
}
|
||||
|
||||
+4
-5
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/restic/restic/internal/feature"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
@@ -50,7 +49,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
|
||||
|
||||
PersistentPreRunE: func(c *cobra.Command, _ []string) error {
|
||||
switch c.Name() {
|
||||
case "__complete", "__completeNoDesc":
|
||||
case cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
|
||||
return nil
|
||||
}
|
||||
return globalOptions.PreRun(needsPassword(c.Name()))
|
||||
@@ -118,7 +117,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
|
||||
// user for authentication).
|
||||
func needsPassword(cmd string) bool {
|
||||
switch cmd {
|
||||
case "cache", "generate", "help", "options", "self-update", "version", "__complete", "__completeNoDesc":
|
||||
case "cache", "generate", "help", "options", "self-update", "version", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
@@ -198,7 +197,7 @@ func main() {
|
||||
|
||||
var exitMessage string
|
||||
switch {
|
||||
case restic.IsAlreadyLocked(err):
|
||||
case repository.IsAlreadyLocked(err):
|
||||
exitMessage = fmt.Sprintf("%v\nthe `unlock` command can be used to remove stale locks", err)
|
||||
case err == ErrInvalidSourceData:
|
||||
exitMessage = fmt.Sprintf("Warning: %v", err)
|
||||
@@ -228,7 +227,7 @@ func main() {
|
||||
exitCode = 3
|
||||
case errors.Is(err, global.ErrNoRepository):
|
||||
exitCode = 10
|
||||
case restic.IsAlreadyLocked(err):
|
||||
case repository.IsAlreadyLocked(err):
|
||||
exitCode = 11
|
||||
case errors.Is(err, repository.ErrNoKeyFound):
|
||||
exitCode = 12
|
||||
|
||||
@@ -281,36 +281,6 @@ after the bucket name like this:
|
||||
be converted to path-style URLs instead, for example ``s3.us-west-2.amazonaws.com/bucket_name``.
|
||||
See below for configuration options for S3-compatible storage from other providers.
|
||||
|
||||
Minio Server
|
||||
************
|
||||
|
||||
`Minio <https://min.io/>`__ is an Open Source Object Storage,
|
||||
written in Go and compatible with Amazon S3 API.
|
||||
|
||||
- Download and Install `Minio Download <https://min.io/download#/linux>`__.
|
||||
- You can also refer to `Minio Docs <https://min.io/docs/minio/linux/>`__ for step by step guidance
|
||||
on installation and getting started on Minio Client and Minio Server.
|
||||
|
||||
You must first setup the following environment variables with the
|
||||
credentials of your Minio Server.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export AWS_ACCESS_KEY_ID=<YOUR-MINIO-ACCESS-KEY-ID>
|
||||
$ export AWS_SECRET_ACCESS_KEY=<YOUR-MINIO-SECRET-ACCESS-KEY>
|
||||
|
||||
Now you can easily initialize restic to use Minio server as a backend with
|
||||
this command.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r s3:http://localhost:9000/restic init
|
||||
enter password for new repository:
|
||||
enter password again:
|
||||
created restic repository 6ad29560f5 at s3:http://localhost:9000/restic
|
||||
Please note that knowledge of your password is required to access
|
||||
the repository. Losing your password means that your data is irrecoverably lost.
|
||||
|
||||
S3-compatible storage
|
||||
*********************
|
||||
|
||||
|
||||
@@ -399,6 +399,9 @@ sign, write ``$$`` to the file - this has to be done even when there's no
|
||||
matching environment variable for the word following a single ``$``. Note
|
||||
that tilde (``~``) is not expanded, instead use the ``$HOME`` or equivalent
|
||||
environment variable (depending on your operating system).
|
||||
For environment variable names with non-alphanumeric characters,
|
||||
enclose the name in curly braces (e.g., ``${ProgramFiles(x86)}``).
|
||||
Without braces, the parser stops at the first non-alphanumeric character.
|
||||
|
||||
Patterns need to match on complete path components. For example, the pattern ``foo``:
|
||||
|
||||
@@ -494,6 +497,8 @@ By specifying the option ``--one-file-system`` you can instruct restic
|
||||
to only backup files from the file systems the initially specified files
|
||||
or directories reside on. In other words, it will prevent restic from crossing
|
||||
filesystem boundaries and subvolumes when performing a backup.
|
||||
Internally, restic identifies a file's filesystem by the "device ID"
|
||||
that is returned by calling lstat() on the file.
|
||||
|
||||
For example, if you backup ``/`` with this option and you have external
|
||||
media mounted under ``/media/usb`` then restic will not back up ``/media/usb``
|
||||
@@ -657,6 +662,12 @@ If there is a **bind-mount** below a directory that is to be saved, restic desce
|
||||
archived as a block device file and restored as such. This also means that the content of the
|
||||
corresponding disk is not read, at least not from the device file.
|
||||
|
||||
**Socket files** are not backed up because they represent active connections by running processes
|
||||
and are only meaningful if created by those processes.
|
||||
|
||||
**FIFO files** are backed up, but because their contents are volatile and never stored on disk,
|
||||
only the file system entry and metadata will be backed up.
|
||||
|
||||
By default, restic does not save the access time (atime) for any files or other
|
||||
items, since it is not possible to reliably disable updating the access time by
|
||||
restic itself. This means that for each new backup a lot of metadata is
|
||||
|
||||
+6
-3
@@ -92,9 +92,12 @@ By default, restic does not restore files as sparse. Use ``restore --sparse`` to
|
||||
enable the creation of sparse files if supported by the filesystem. Then restic
|
||||
will restore long runs of zero bytes as holes in the corresponding files.
|
||||
Reading from a hole returns the original zero bytes, but it does not consume
|
||||
disk space. Note that the exact location of the holes can differ from those in
|
||||
the original file, as their location is determined while restoring and is not
|
||||
stored explicitly.
|
||||
disk space. Note that restic does not keep track of which files were originally
|
||||
sparse or at what locations sparse holes were. When restoring, restic will create
|
||||
a sparse hole (by seeking forward instead of writing data) for any range of zeros
|
||||
at the start of a blob. This range can span the entire blob. Because large files
|
||||
result in blobs much larger than file system blocks, restoring a large sparse file
|
||||
with many holes may make it significantly larger than the original sparse file.
|
||||
|
||||
Restoring extended file attributes
|
||||
----------------------------------
|
||||
|
||||
+3
-1
@@ -323,7 +323,7 @@ five days that had a snapshot and remove the other snapshots:
|
||||
keep 5 snapshots:
|
||||
ID Time Host Tags Reasons Paths
|
||||
-------------------------------------------------------------------------------
|
||||
8a603ddf 2025-04-25 23:00:00 mopped daily snapshot /home/user/work
|
||||
ae33bf10 2025-04-25 23:00:00 mopped daily snapshot /home/user/work
|
||||
fb8b1418 2025-04-28 11:00:00 mopped daily snapshot /home/user/work
|
||||
1db188b4 2025-04-29 11:00:00 mopped daily snapshot /home/user/work
|
||||
7baa4f28 2025-05-01 11:00:00 mopped daily snapshot /home/user/work
|
||||
@@ -507,6 +507,8 @@ The ``prune`` command accepts the following options:
|
||||
|
||||
- ``--verbose`` increased verbosity shows additional statistics for ``prune``.
|
||||
|
||||
- ``--json`` gives the statistics in JSON format.
|
||||
|
||||
|
||||
Recovering from "no free space" errors
|
||||
**************************************
|
||||
|
||||
+113
-2
@@ -41,6 +41,8 @@ environment variables, which are listed below.
|
||||
RESTIC_PROGRESS_FPS Frames per second by which the progress bar is updated
|
||||
RESTIC_PACK_SIZE Target size for pack files
|
||||
RESTIC_READ_CONCURRENCY Concurrency for file reads
|
||||
RESTIC_IGNORE_CTIME Ignore ctime changes when comparing files (replaces --ignore-ctime)
|
||||
RESTIC_IGNORE_INODE Ignore inode changes when comparing files (replaces --ignore-inode)
|
||||
|
||||
RESTIC_FROM_REPOSITORY Source repository for copy (replaces --from-repo)
|
||||
RESTIC_FROM_REPOSITORY_FILE File containing source repository for copy (replaces --from-repository-file)
|
||||
@@ -241,6 +243,13 @@ Several commands, in particular long running ones or those that generate a large
|
||||
use a format also known as JSON lines. It consists of a stream of new-line separated JSON
|
||||
messages. You can determine the nature of the message using the ``message_type`` field.
|
||||
|
||||
Messages with type ``status`` are emitted at a regular interval while the command runs,
|
||||
by default about ten times per second, regardless of whether the output is a terminal
|
||||
or a pipe. Passing ``--quiet`` disables them. The ``RESTIC_PROGRESS_FPS`` environment
|
||||
variable overrides both: it sets the number of status updates per second and re-enables
|
||||
them even when ``--quiet`` is set. Values below 1 are allowed, for example ``0.0166``
|
||||
results in roughly one status message per minute, which is useful when capturing logs.
|
||||
|
||||
backup
|
||||
------
|
||||
|
||||
@@ -311,6 +320,15 @@ Verbose status provides details about the progress, including details about back
|
||||
| ``total_files`` | Total number of files | uint64 |
|
||||
+---------------------------+----------------------------------------------------------+---------+
|
||||
|
||||
Excluded backup items are detailed in this format:
|
||||
|
||||
+---------------------------+----------------------------------------------------------+---------+
|
||||
| ``message_type`` | Always "excluded_item" | string |
|
||||
+---------------------------+----------------------------------------------------------+---------+
|
||||
| ``item`` | The item in question | string |
|
||||
+---------------------------+----------------------------------------------------------+---------+
|
||||
|
||||
|
||||
Summary
|
||||
^^^^^^^
|
||||
|
||||
@@ -534,8 +552,7 @@ forget
|
||||
The ``forget`` command prints a single JSON document containing an array of
|
||||
ForgetGroups. If specific snapshot IDs are specified, then no output is generated.
|
||||
|
||||
The ``prune`` command does not yet support JSON such that ``forget --prune``
|
||||
results in a mix of JSON and text output.
|
||||
When using the ``--prune`` option, its output is appended in the JSON lines format.
|
||||
|
||||
ForgetGroup
|
||||
^^^^^^^^^^^
|
||||
@@ -599,6 +616,100 @@ KeepReason object
|
||||
+--------------+--------------------------------------------------------+--------------------+
|
||||
|
||||
|
||||
prune
|
||||
-----
|
||||
|
||||
The ``prune`` command uses the JSON lines format, but only outputs a single message.
|
||||
|
||||
+------------------+----------------------------------------+--------------------------+
|
||||
| ``message_type`` | Always "summary" | string |
|
||||
+------------------+----------------------------------------+--------------------------+
|
||||
| ``blobs`` | Statistics regarding data blobs | `PruneBlobs object`_ |
|
||||
+------------------+----------------------------------------+--------------------------+
|
||||
| ``bytes`` | Statistics regarding sizes in bytes | `PruneSizes object`_ |
|
||||
+------------------+----------------------------------------+--------------------------+
|
||||
| ``packfiles`` | Statistics regarding packfiles | `PrunePackfiles object`_ |
|
||||
+------------------+----------------------------------------+--------------------------+
|
||||
|
||||
.. _PruneBlobs object:
|
||||
|
||||
PruneBlobs object
|
||||
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``used`` | Number of used blobs | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``duplicate`` | Number of duplicate blobs | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``unused`` | Number of unused blobs | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``total`` | Total number of blobs | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``repack`` | Number of blobs to be repacked | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``repack_remove`` | Number of blobs removed by repacking | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``remove`` | Number of blobs removed by pack deletion | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``remove_total`` | Total number of blobs to be removed | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
| ``remaining`` | Number of blobs remaining | uint |
|
||||
+-----------------+----------------------------------------+------+
|
||||
|
||||
.. _PruneSizes object:
|
||||
|
||||
PruneSizes object
|
||||
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``used`` | Size of used blobs | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``duplicate`` | Size of duplicate blobs | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``unused`` | Size of unused blobs | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``unreferenced`` | Size of unreferenced pack files | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``uncompressed`` | Size of uncompressed pack files | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``total`` | Total size of blobs | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``repack`` | Size of blobs to be repacked | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``repack_remove`` | Size of blobs removed by repacking | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``remove`` | Size of blobs removed by pack deletion | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``remove_total`` | Total size of blobs to be removed | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``remaining`` | Size of blobs remaining | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
| ``remaining_unused`` | Size of remaining unused blobs | uint64 |
|
||||
+--------------------+-------------------------------------+--------+
|
||||
|
||||
.. _PrunePackfiles object:
|
||||
|
||||
PrunePackfiles object
|
||||
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``used`` | Number of used pack files | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``unused`` | Number of unused pack files | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``partly_used`` | Number of partially used pack files | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``unreferenced`` | Number of unreferenced pack files | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``total`` | Total number of pack files | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``keep`` | Number of pack files to keep | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``repack`` | Number of pack files to repack | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``remove`` | Number of pack files to remove | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
| ``remove_total`` | Total number of pack files to remove | uint |
|
||||
+------------------+---------------------------------------+------+
|
||||
|
||||
|
||||
init
|
||||
----
|
||||
|
||||
|
||||
@@ -80,11 +80,15 @@ Similarly, if a repository is repeatedly damaged, please open an `issue on GitHu
|
||||
somewhere. Please include the check output and additional information that might
|
||||
help locate the problem.
|
||||
|
||||
If ``check`` detects damaged pack files, it will show instructions on how to repair
|
||||
When ``restic check`` detects damaged or missing packfiles, it will show instructions on how to repair
|
||||
them using the ``repair packs`` command. Use that command instead of the "Repairing the
|
||||
index" section in this guide.
|
||||
|
||||
If you are interested to check only specific snapshots, you can now
|
||||
If ``check`` detects unreadable snapshot files, it will show instructions on how to repair
|
||||
them using the ``repair snapshots`` command. Follow those instructions as part of the
|
||||
"Removing broken snapshots" section in this guide.
|
||||
|
||||
If you are interested to check only specific snapshots, you can
|
||||
use the standard snapshot filter method specifying ``--host``, ``--path``, ``--tag`` or
|
||||
alternatively naming snapshot ID(s) explicitly. The selected subset of packfiles
|
||||
will then be checked for consistency and read when either ``--read-data`` or
|
||||
@@ -148,8 +152,39 @@ repair the index first!
|
||||
Please note that it is not recommended to repair the index unless the repository
|
||||
is actually damaged.
|
||||
|
||||
4. Removing broken snapshots
|
||||
****************************
|
||||
|
||||
4. Running all backups (optional)
|
||||
.. note::
|
||||
|
||||
This step is only necessary if the ``check`` command tells you to run ``restic repair snapshots``.
|
||||
|
||||
In case of damage to a snapshot file, ``check`` will show an error message like the following:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic check
|
||||
using temporary cache in /tmp/restic-check-cache-2150939789
|
||||
create exclusive lock for repository
|
||||
repository cfabc5ed opened (version 1)
|
||||
created new cache in /tmp/restic-check-cache-2150939789
|
||||
load indexes
|
||||
[0:00] 100.00% 1 / 1 index files loaded
|
||||
check all packs
|
||||
check snapshots, trees and blobs
|
||||
error: failed to load snapshot 1d204771: LoadRaw(<snapshot/1d20477115>): invalid data returned
|
||||
[0:00] 100.00% 1 / 1 snapshots
|
||||
|
||||
The repository contains damaged snapshot files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.
|
||||
|
||||
restic repair snapshots --forget 1d2047711588c657efea246369c499bb2133240b1e03477d503386ceaa92fa2f
|
||||
|
||||
Damaged snapshot files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!
|
||||
Fatal: repository contains errors
|
||||
|
||||
As explained in the command output, you have to run ``restic repair snapshots --forget 1d2047711588c657efea246369c499bb2133240b1e03477d503386ceaa92fa2f`` to remove the broken snapshot file.
|
||||
|
||||
5. Running all backups (optional)
|
||||
*********************************
|
||||
|
||||
With a correct index, the ``backup`` command guarantees that newly created
|
||||
@@ -165,7 +200,7 @@ To check if the repository is fully repaired, you can run ``restic check``
|
||||
To get a list of still damaged files, you can run ``restic repair snapshots --dry-run``.
|
||||
Look for ``would save new snapshot`` messages to find affected snapshots.
|
||||
|
||||
5. Removing missing data from snapshots
|
||||
6. Removing missing data from snapshots
|
||||
***************************************
|
||||
|
||||
If your repository is still missing data, then you can use the ``repair snapshots``
|
||||
@@ -190,8 +225,7 @@ If you did not add the ``--forget`` option, then you have to manually delete all
|
||||
modified snapshots using the ``forget`` command. In the example above, you'd have
|
||||
to run ``restic forget 6979421e``.
|
||||
|
||||
|
||||
6. Checking the repository again
|
||||
7. Checking the repository again
|
||||
********************************
|
||||
|
||||
As a final step, run ``check`` again to make sure that the repository has been successfully
|
||||
|
||||
@@ -184,7 +184,8 @@ default backup, to be found at ``cmd/restic/testdata/backup-data.tar.gz``.
|
||||
In this compressed archive you will find files, hardlinked files,
|
||||
symlinked files, an empty directory and a simple directory structure which is good for testing purposes.
|
||||
|
||||
Commands that require a ``progress.Printer`` should either be wrapped in ``withTermStatus`` or ``withCaptureStdout``.
|
||||
Commands that require a ``restic.Printer`` should either be wrapped in ``withTermStatus`` or ``withCaptureStdout``.
|
||||
The interface is defined in ``internal/restic``; ``progress.NewTerminalPrinter`` returns a ``restic.Printer``.
|
||||
If you want to analyze JSON output, you use ``withCaptureStdout()``.
|
||||
It returns the generated output in a ``*bytes.Buffer``.
|
||||
JSON output can be unmarshalled to produce the appropriate go structures; see
|
||||
@@ -236,7 +237,7 @@ Security
|
||||
possible critical security problem, please do *not* open a GitHub issue
|
||||
but send an email directly to alexander@bumpern.de. If possible, please
|
||||
encrypt your email using the following PGP key
|
||||
(`0x91A6868BD3F7A907 <https://pgp.mit.edu/pks/lookup?op=get&search=0xCF8F18F2844575973F79D4E191A6868BD3F7A907>`__):
|
||||
(`0x91A6868BD3F7A907 <https://restic.net/gpg-key-alex.asc>`__):
|
||||
|
||||
::
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ Talks
|
||||
|
||||
The following talks will be or have been given about restic:
|
||||
|
||||
- 2026-06-10: `restic on the FLOSS Weekly podcast <https://hackaday.com/2026/06/10/floss-weekly-episode-870-open-source-gardening/>`__
|
||||
- 2021-04-02: `The Changelog: Restic has your backup
|
||||
(Podcast) <https://changelog.com/podcast/434>`__
|
||||
- 2016-01-31: Lightning Talk at the Go Devroom at FOSDEM 2016,
|
||||
|
||||
+7
-6
@@ -47,12 +47,13 @@ comparing its output to the file name. If the prefix of a filename is
|
||||
unique amongst all the other files in the same directory, the prefix may
|
||||
be used instead of the complete filename.
|
||||
|
||||
Apart from the files stored within the ``keys`` and ``data`` directories,
|
||||
all files are encrypted with AES-256 in counter mode (CTR). The integrity
|
||||
of the encrypted data is secured by a Poly1305-AES message authentication
|
||||
code (MAC).
|
||||
Files in the ``data`` directory ("pack files") consist of multiple parts
|
||||
which are all independently encrypted and authenticated, see below.
|
||||
Apart from the files stored within the ``keys`` directory, all files
|
||||
are encrypted with AES-256 in counter mode (CTR). The integrity of the
|
||||
encrypted data is secured by a
|
||||
Poly1305-AES message authentication code (MAC).
|
||||
Files in the ``data`` directory ("pack files") consist of multiple
|
||||
independently encrypted and authenticated parts: the individual blobs as
|
||||
well as the pack header, see below.
|
||||
|
||||
In the first 16 bytes of each encrypted file the initialisation vector
|
||||
(IV) is stored. It is followed by the encrypted data and completed by
|
||||
|
||||
@@ -291,6 +291,7 @@ Archive** storage classes is available:
|
||||
- Currently, only the following commands are known to work:
|
||||
|
||||
- ``backup``
|
||||
- ``check``
|
||||
- ``copy``
|
||||
- ``prune``
|
||||
- ``restore``
|
||||
|
||||
+14
-1
@@ -30,7 +30,8 @@ The allowed types are (in alphabetic order):
|
||||
- :ref:`list_index`
|
||||
- :ref:`keys <list_keys_locks_packs>`
|
||||
- :ref:`locks <list_keys_locks_packs>`
|
||||
- :ref:`packs <list_keys_locks_packs>`
|
||||
- :ref:`packs<list_keys_locks_packs>`
|
||||
- :ref:`packs [snapshot ID]<list_keys_locks_packs>`
|
||||
- :ref:`list_snapshots`
|
||||
|
||||
|
||||
@@ -160,6 +161,18 @@ Here is an example which lists all the packs in the repository:
|
||||
953e5381138bdc44da23740a83065809dd4021f45ce4e351b577dc4c07f81314
|
||||
75bca8556f47d16362e58e757ea89a34b28fb96aedcc314bea35d468e5cb665c
|
||||
|
||||
If you want to list all packfiles which are part of a snapshot, use the command
|
||||
``list packs`` and attach the snapshot ID of the snapshot you want to see.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /srv/restic-repo list packs latest -q
|
||||
ae92415f79c281330159ed590db2a973048c886aacfa4fabfa0eaac10b396b8f
|
||||
dc25893d422a71af41aaaa843cd7121708cd88679bf3885f94a32900ac068e84
|
||||
|
||||
This will give you the list of all packfiles, sorted in ID ascending order.
|
||||
|
||||
|
||||
.. _view-repository-objects:
|
||||
|
||||
Inspecting repository objects
|
||||
|
||||
@@ -9,10 +9,10 @@ toolchain go1.25.10
|
||||
godebug winsymlink=0
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.62.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0
|
||||
cloud.google.com/go/storage v1.64.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0
|
||||
github.com/Backblaze/blazer v0.7.2
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/anacrolix/fuse v0.3.1
|
||||
@@ -22,42 +22,42 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/klauspost/compress v1.18.6
|
||||
github.com/minio/minio-go/v7 v7.1.0
|
||||
github.com/klauspost/compress v1.19.2
|
||||
github.com/minio/minio-go/v7 v7.2.1
|
||||
github.com/ncw/swift/v2 v2.0.5
|
||||
github.com/peterbourgon/unixtransport v0.0.7
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/pkg/sftp v1.13.11
|
||||
github.com/pkg/xattr v0.4.12
|
||||
github.com/restic/chunker v0.4.0
|
||||
github.com/restic/chunker v0.5.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/term v0.43.0
|
||||
golang.org/x/text v0.37.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
golang.org/x/text v0.40.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/api v0.282.0
|
||||
google.golang.org/api v0.291.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.20.0 // indirect
|
||||
cloud.google.com/go/auth v0.22.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.7.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
cloud.google.com/go/iam v1.11.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.29.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -65,7 +65,6 @@ require (
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -73,8 +72,8 @@ require (
|
||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
@@ -91,18 +90,19 @@ require (
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.2 // indirect
|
||||
)
|
||||
|
||||
@@ -2,50 +2,50 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||
cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ=
|
||||
cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
|
||||
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
|
||||
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
|
||||
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
|
||||
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
|
||||
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4=
|
||||
cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
|
||||
cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
|
||||
cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k=
|
||||
cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI=
|
||||
cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
|
||||
cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
|
||||
cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY=
|
||||
cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM=
|
||||
cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg=
|
||||
cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ=
|
||||
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
|
||||
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0 h1:BM85pSYlVYQHdq00nxyPoOkyLF5NArJG3bOsrmbwr4k=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0/go.mod h1:QYjP2cB7ZYtS/8jAbE0VSBZde/tjExqGjp+8JY6/+ts=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0 h1:irsmOWwkp0KCTTNS5e2hdFeIvSQClQo2No3IaNmL3Vw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0/go.mod h1:GWcBkQj3MqN7ozHKLaCCAuNLiXoIGv2RtanfAwSjY/Y=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM=
|
||||
github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
|
||||
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
@@ -93,8 +93,6 @@ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNu
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -120,10 +118,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
|
||||
github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
|
||||
github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
@@ -131,8 +129,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
@@ -155,8 +153,8 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8=
|
||||
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
|
||||
github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
|
||||
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
|
||||
github.com/ncw/swift/v2 v2.0.5 h1:9o5Gsd7bInAFEqsGPcaUdsboMbqf8lnNtxqWKFT9iz8=
|
||||
github.com/ncw/swift/v2 v2.0.5/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
@@ -173,8 +171,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=
|
||||
github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
@@ -184,8 +182,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/restic/chunker v0.4.0 h1:YUPYCUn70MYP7VO4yllypp2SjmsRhRJaad3xKu1QFRw=
|
||||
github.com/restic/chunker v0.4.0/go.mod h1:z0cH2BejpW636LXw0R/BGyv+Ey8+m9QGiOanDHItzyw=
|
||||
github.com/restic/chunker v0.5.0 h1:1y+ut0MBduzxODJ298rhQCtESoEpj8v1hTydZlKaE1Y=
|
||||
github.com/restic/chunker v0.5.0/go.mod h1:z0cH2BejpW636LXw0R/BGyv+Ey8+m9QGiOanDHItzyw=
|
||||
github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
@@ -207,11 +205,13 @@ github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9C
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
@@ -226,24 +226,26 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
@@ -251,8 +253,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
|
||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
@@ -265,16 +267,16 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -288,20 +290,20 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -314,16 +316,16 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I=
|
||||
google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/api v0.291.0 h1:wfPbbY+mr9c7wZLqqzrHJLft/q8iFKREd6IgTBUene0=
|
||||
google.golang.org/api v0.291.0/go.mod h1:at7kwWbuonglBFEBoeMDAV1bguHqL3qf0BHFsv3coa0=
|
||||
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 h1:YJjbgu+dkp5kUJLfpMyCLfBIWZb/FcJyuLeo1gVBOuo=
|
||||
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -332,6 +334,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
|
||||
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -39,7 +39,7 @@ func init() {
|
||||
pflag.Parse()
|
||||
}
|
||||
|
||||
func die(f string, args ...interface{}) {
|
||||
func die(f string, args ...any) {
|
||||
if !strings.HasSuffix(f, "\n") {
|
||||
f += "\n"
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func die(f string, args ...interface{}) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func msg(f string, args ...interface{}) {
|
||||
func msg(f string, args ...any) {
|
||||
if !strings.HasSuffix(f, "\n") {
|
||||
f += "\n"
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func msg(f string, args ...interface{}) {
|
||||
fmt.Printf(f, args...)
|
||||
}
|
||||
|
||||
func verbose(f string, args ...interface{}) {
|
||||
func verbose(f string, args ...any) {
|
||||
if !opts.Verbose {
|
||||
return
|
||||
}
|
||||
@@ -207,10 +207,7 @@ func buildForTarget(sourceDir, outputDir, goos, goarch string) (filename string)
|
||||
func buildTargets(sourceDir, outputDir string, targets map[string][]string) {
|
||||
start := time.Now()
|
||||
// the go compiler is already parallelized, thus reduce the concurrency a bit
|
||||
workers := runtime.GOMAXPROCS(0) / 4
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
workers := max(runtime.GOMAXPROCS(0)/4, 1)
|
||||
msg("building with %d workers", workers)
|
||||
|
||||
type Job struct{ GOOS, GOARCH string }
|
||||
@@ -218,7 +215,7 @@ func buildTargets(sourceDir, outputDir string, targets map[string][]string) {
|
||||
var wg errgroup.Group
|
||||
ch := make(chan Job)
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
for range workers {
|
||||
wg.Go(func() error {
|
||||
for job := range ch {
|
||||
start := time.Now()
|
||||
|
||||
@@ -43,7 +43,7 @@ func init() {
|
||||
pflag.Parse()
|
||||
}
|
||||
|
||||
func die(f string, args ...interface{}) {
|
||||
func die(f string, args ...any) {
|
||||
if !strings.HasSuffix(f, "\n") {
|
||||
f += "\n"
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func die(f string, args ...interface{}) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func msg(f string, args ...interface{}) {
|
||||
func msg(f string, args ...any) {
|
||||
if !strings.HasSuffix(f, "\n") {
|
||||
f += "\n"
|
||||
}
|
||||
@@ -428,11 +428,11 @@ func updateDocker(sourceDir, version string) string {
|
||||
buildCmd := fmt.Sprintf("docker buildx build --builder %s --platform linux/386,linux/amd64,linux/arm,linux/arm64 --pull -f docker/Dockerfile.release %q", builderName, sourceDir)
|
||||
run("sh", "-c", buildCmd+" --no-cache")
|
||||
|
||||
publishCmds := ""
|
||||
var publishCmds strings.Builder
|
||||
for _, tag := range []string{"restic/restic:latest", "restic/restic:" + version} {
|
||||
publishCmds += buildCmd + fmt.Sprintf(" --tag %q --push\n", tag)
|
||||
publishCmds.WriteString(buildCmd + fmt.Sprintf(" --tag %q --push\n", tag))
|
||||
}
|
||||
return publishCmds + "\ndocker buildx rm " + builderName
|
||||
return publishCmds.String() + "\ndocker buildx rm " + builderName
|
||||
}
|
||||
|
||||
func tempdir(prefix string) string {
|
||||
|
||||
@@ -67,8 +67,8 @@ func (s *ItemStats) Add(other ItemStats) {
|
||||
s.TreeSizeInRepo += other.TreeSizeInRepo
|
||||
}
|
||||
|
||||
// ToNoder returns a data.Node for a File.
|
||||
type ToNoder interface {
|
||||
// toNoder returns a data.Node for a File.
|
||||
type toNoder interface {
|
||||
ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*data.Node, error)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ type archiverRepo interface {
|
||||
restic.WithBlobUploader
|
||||
restic.SaverUnpacked[restic.WriteableFileType]
|
||||
|
||||
Config() restic.Config
|
||||
ChunkerFactory() restic.ChunkerFactory
|
||||
}
|
||||
|
||||
// Archiver saves a directory structure to the repo.
|
||||
@@ -110,11 +110,11 @@ type Archiver struct {
|
||||
// particular file/dir.
|
||||
//
|
||||
// Once reading a file has completed successfully (but not saving it yet),
|
||||
// CompleteItem will be called with current == nil.
|
||||
// CompleteItem will be called with a zero ItemAction.
|
||||
//
|
||||
// CompleteItem may be called asynchronously from several different
|
||||
// goroutines!
|
||||
CompleteItem func(item string, previous, current *data.Node, s ItemStats, d time.Duration)
|
||||
CompleteItem func(item string, action ItemAction, s ItemStats, d time.Duration)
|
||||
|
||||
// StartFile is called when a file is being processed by a worker.
|
||||
StartFile func(filename string)
|
||||
@@ -129,6 +129,9 @@ type Archiver struct {
|
||||
|
||||
// Flags controlling change detection. See doc/040_backup.rst for details.
|
||||
ChangeIgnoreFlags uint
|
||||
|
||||
// for excluded items
|
||||
ExcludedItem func(path string)
|
||||
}
|
||||
|
||||
// Flags for the ChangeIgnoreFlags bitfield.
|
||||
@@ -149,9 +152,9 @@ type Options struct {
|
||||
SaveTreeConcurrency uint
|
||||
}
|
||||
|
||||
// ApplyDefaults returns a copy of o with the default options set for all unset
|
||||
// applyDefaults returns a copy of o with the default options set for all unset
|
||||
// fields.
|
||||
func (o Options) ApplyDefaults() Options {
|
||||
func (o Options) applyDefaults() Options {
|
||||
if o.ReadConcurrency == 0 {
|
||||
// two is a sweet spot for almost all situations. We've done some
|
||||
// experiments documented here:
|
||||
@@ -178,11 +181,12 @@ func New(repo archiverRepo, filesystem fs.FS, opts Options) *Archiver {
|
||||
SelectByName: func(_ string) bool { return true },
|
||||
Select: func(_ string, _ *fs.ExtendedFileInfo, _ fs.FS) bool { return true },
|
||||
FS: filesystem,
|
||||
Options: opts.ApplyDefaults(),
|
||||
Options: opts.applyDefaults(),
|
||||
|
||||
CompleteItem: func(string, *data.Node, *data.Node, ItemStats, time.Duration) {},
|
||||
CompleteItem: func(string, ItemAction, ItemStats, time.Duration) {},
|
||||
StartFile: func(string) {},
|
||||
CompleteBlob: func(uint64) {},
|
||||
ExcludedItem: func(string) {},
|
||||
}
|
||||
|
||||
return arch
|
||||
@@ -211,45 +215,40 @@ func (arch *Archiver) error(item string, err error) error {
|
||||
}
|
||||
|
||||
func (arch *Archiver) trackItem(item string, previous, current *data.Node, s ItemStats, d time.Duration) {
|
||||
arch.CompleteItem(item, previous, current, s, d)
|
||||
action := itemProgressAction(previous, current)
|
||||
arch.CompleteItem(item, action, s, d)
|
||||
|
||||
arch.mu.Lock()
|
||||
defer arch.mu.Unlock()
|
||||
|
||||
arch.summary.ItemStats.Add(s)
|
||||
|
||||
if current != nil {
|
||||
arch.summary.ProcessedBytes += current.Size
|
||||
} else {
|
||||
if action.IsZero() {
|
||||
// last item or an error occurred
|
||||
return
|
||||
}
|
||||
|
||||
switch current.Type {
|
||||
case data.NodeTypeDir:
|
||||
switch {
|
||||
case previous == nil:
|
||||
arch.summary.Dirs.New++
|
||||
case previous.Equals(*current):
|
||||
arch.summary.Dirs.Unchanged++
|
||||
default:
|
||||
arch.summary.Dirs.Changed++
|
||||
}
|
||||
arch.summary.ProcessedBytes += current.Size
|
||||
|
||||
case data.NodeTypeFile:
|
||||
switch {
|
||||
case previous == nil:
|
||||
arch.summary.Files.New++
|
||||
case previous.Equals(*current):
|
||||
arch.summary.Files.Unchanged++
|
||||
default:
|
||||
arch.summary.Files.Changed++
|
||||
}
|
||||
switch action {
|
||||
case ActionDirNew:
|
||||
arch.summary.Dirs.New++
|
||||
case ActionDirUnchanged:
|
||||
arch.summary.Dirs.Unchanged++
|
||||
case ActionDirModified:
|
||||
arch.summary.Dirs.Changed++
|
||||
|
||||
case ActionFileNew:
|
||||
arch.summary.Files.New++
|
||||
case ActionFileUnchanged:
|
||||
arch.summary.Files.Unchanged++
|
||||
case ActionFileModified:
|
||||
arch.summary.Files.Changed++
|
||||
}
|
||||
}
|
||||
|
||||
// nodeFromFileInfo returns the restic node from an os.FileInfo.
|
||||
func (arch *Archiver) nodeFromFileInfo(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*data.Node, error) {
|
||||
func (arch *Archiver) nodeFromFileInfo(snPath, filename string, meta toNoder, ignoreXattrListError bool) (*data.Node, error) {
|
||||
node, err := meta.ToNode(ignoreXattrListError, func(format string, args ...any) {
|
||||
_ = arch.error(filename, fmt.Errorf(format, args...))
|
||||
})
|
||||
@@ -297,7 +296,7 @@ func (arch *Archiver) loadSubtree(ctx context.Context, node *data.Node) (data.Tr
|
||||
}
|
||||
|
||||
func (arch *Archiver) wrapLoadTreeError(id restic.ID, err error) error {
|
||||
if _, ok := arch.Repo.LookupBlobSize(restic.TreeBlob, id); ok {
|
||||
if _, ok := arch.Repo.LookupBlobSize(restic.BlobHandle{Type: restic.TreeBlob, ID: id}); ok {
|
||||
err = errors.Errorf("tree %v could not be loaded; the repository could be damaged: %v", id, err)
|
||||
} else {
|
||||
err = errors.Errorf("tree %v is not known; the repository could be damaged, run `repair index` to try to repair it", id)
|
||||
@@ -444,7 +443,7 @@ func (fn *futureNode) take(ctx context.Context) futureNodeResult {
|
||||
func (arch *Archiver) allBlobsPresent(previous *data.Node) bool {
|
||||
// check if all blobs are contained in index
|
||||
for _, id := range previous.Content {
|
||||
if _, ok := arch.Repo.LookupBlobSize(restic.DataBlob, id); !ok {
|
||||
if _, ok := arch.Repo.LookupBlobSize(restic.BlobHandle{Type: restic.DataBlob, ID: id}); !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -486,6 +485,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
|
||||
// exclude files by path before running Lstat to reduce number of lstat calls
|
||||
if !explicit && !arch.SelectByName(abstarget) {
|
||||
debug.Log("%v is excluded by path", target)
|
||||
arch.ExcludedItem(abstarget)
|
||||
return futureNode{}, true, nil
|
||||
}
|
||||
|
||||
@@ -514,6 +514,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
|
||||
}
|
||||
if !explicit && !arch.Select(abstarget, fi, arch.FS) {
|
||||
debug.Log("%v is excluded", target)
|
||||
arch.ExcludedItem(abstarget)
|
||||
return futureNode{}, true, nil
|
||||
}
|
||||
|
||||
@@ -790,9 +791,9 @@ func (arch *Archiver) dirPathToNode(snPath, target string) (node *data.Node, err
|
||||
//
|
||||
// Paths returned with Explicit true are those the user listed literally; paths
|
||||
// inserted from directory expansion have Explicit false.
|
||||
func resolveRelativeTargets(filesys fs.FS, targets []string) ([]BackupTarget, error) {
|
||||
func resolveRelativeTargets(filesys fs.FS, targets []string) ([]backupTarget, error) {
|
||||
debug.Log("targets before resolving: %v", targets)
|
||||
result := make([]BackupTarget, 0, len(targets))
|
||||
result := make([]backupTarget, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
if target != "" && filesys.VolumeName(target) == target {
|
||||
// special case to allow users to also specify a volume name "C:" instead of a path "C:\"
|
||||
@@ -802,7 +803,7 @@ func resolveRelativeTargets(filesys fs.FS, targets []string) ([]BackupTarget, er
|
||||
}
|
||||
pc, _ := pathComponents(filesys, target, false)
|
||||
if len(pc) > 0 {
|
||||
result = append(result, BackupTarget{Path: target, Explicit: true})
|
||||
result = append(result, backupTarget{Path: target, Explicit: true})
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -814,7 +815,7 @@ func resolveRelativeTargets(filesys fs.FS, targets []string) ([]BackupTarget, er
|
||||
sort.Strings(entries)
|
||||
|
||||
for _, name := range entries {
|
||||
result = append(result, BackupTarget{
|
||||
result = append(result, backupTarget{
|
||||
Path: filesys.Join(target, name),
|
||||
Explicit: false,
|
||||
})
|
||||
@@ -863,7 +864,7 @@ func (arch *Archiver) loadParentTree(ctx context.Context, sn *data.Snapshot) dat
|
||||
func (arch *Archiver) runWorkers(ctx context.Context, wg *errgroup.Group, uploader restic.BlobSaverAsync) {
|
||||
arch.fileSaver = newFileSaver(ctx, wg,
|
||||
uploader,
|
||||
arch.Repo.Config().ChunkerPolynomial,
|
||||
arch.Repo.ChunkerFactory(),
|
||||
arch.Options.ReadConcurrency)
|
||||
arch.fileSaver.CompleteBlob = arch.CompleteBlob
|
||||
arch.fileSaver.NodeFromFileInfo = arch.nodeFromFileInfo
|
||||
|
||||
@@ -132,11 +132,10 @@ func TestArchiverSaveFile(t *testing.T) {
|
||||
|
||||
for _, testfile := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, TestDir{"file": testfile})
|
||||
node, stats := saveFile(t, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.Local{}})
|
||||
node, stats := saveFile(t, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.NewLocal()})
|
||||
|
||||
TestEnsureFileContent(ctx, t, repo, "file", node, testfile)
|
||||
if stats.DataSize != uint64(len(testfile.Content)) {
|
||||
@@ -165,8 +164,7 @@ func TestArchiverSaveFileReaderFS(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
repo := repository.TestRepository(t)
|
||||
|
||||
@@ -206,12 +204,11 @@ func TestArchiverSave(t *testing.T) {
|
||||
|
||||
for _, testfile := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, TestDir{"file": testfile})
|
||||
|
||||
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
|
||||
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
|
||||
arch.Error = func(item string, err error) error {
|
||||
t.Errorf("archiver error for %v: %v", item, err)
|
||||
return err
|
||||
@@ -276,8 +273,7 @@ func TestArchiverSaveReaderFS(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
repo := repository.TestRepository(t)
|
||||
|
||||
@@ -357,7 +353,7 @@ func BenchmarkArchiverSaveFileSmall(b *testing.B) {
|
||||
tempdir, repo := prepareTempdirRepoSrc(b, d)
|
||||
b.StartTimer()
|
||||
|
||||
_, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.Local{}})
|
||||
_, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.NewLocal()})
|
||||
|
||||
b.StopTimer()
|
||||
if stats.DataSize != fileSize {
|
||||
@@ -389,7 +385,7 @@ func BenchmarkArchiverSaveFileLarge(b *testing.B) {
|
||||
tempdir, repo := prepareTempdirRepoSrc(b, d)
|
||||
b.StartTimer()
|
||||
|
||||
_, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.Local{}})
|
||||
_, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{FS: fs.NewLocal()})
|
||||
|
||||
b.StopTimer()
|
||||
if stats.DataSize != fileSize {
|
||||
@@ -477,9 +473,9 @@ func TestArchiverSaveFileIncremental(t *testing.T) {
|
||||
data := rtest.Random(23, 512*1024+887898)
|
||||
testfile := filepath.Join(tempdir, "testfile")
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := range 3 {
|
||||
appendToFile(t, testfile, data)
|
||||
node, _ := saveFile(t, repo, testfile, fs.Track{FS: fs.Local{}})
|
||||
node, _ := saveFile(t, repo, testfile, fs.Track{FS: fs.NewLocal()})
|
||||
|
||||
t.Logf("node blobs: %v", node.Content)
|
||||
|
||||
@@ -572,13 +568,7 @@ func nodeFromFile(t testing.TB, localFs fs.FS, filename string) *data.Node {
|
||||
|
||||
// sleep sleeps long enough to ensure a timestamp change.
|
||||
func sleep() {
|
||||
d := 50 * time.Millisecond
|
||||
if runtime.GOOS == "darwin" {
|
||||
// On older Darwin instances, the file system only supports one second
|
||||
// granularity.
|
||||
d = 1500 * time.Millisecond
|
||||
}
|
||||
time.Sleep(d)
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestFileChanged(t *testing.T) {
|
||||
@@ -691,7 +681,7 @@ func TestFileChanged(t *testing.T) {
|
||||
}
|
||||
save(t, filename, content)
|
||||
|
||||
fs := &fs.Local{}
|
||||
fs := fs.NewLocal()
|
||||
fiBefore, err := fs.Lstat(filename)
|
||||
rtest.OK(t, err)
|
||||
node := nodeFromFile(t, fs, filename)
|
||||
@@ -735,7 +725,7 @@ func TestFilChangedSpecialCases(t *testing.T) {
|
||||
|
||||
t.Run("type-change", func(t *testing.T) {
|
||||
fi := lstat(t, filename)
|
||||
node := nodeFromFile(t, &fs.Local{}, filename)
|
||||
node := nodeFromFile(t, fs.NewLocal(), filename)
|
||||
node.Type = data.NodeTypeSymlink
|
||||
if !fileChanged(fi, node, 0) {
|
||||
t.Fatal("node with changed type detected as unchanged")
|
||||
@@ -754,12 +744,12 @@ func TestArchiverSaveDir(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
src: TestDir{
|
||||
"targetfile": TestFile{Content: string(rtest.Random(888, 2*1024*1024+5000))},
|
||||
"targetfile": TestFile{Content: string(rtest.Random(888, 20*1024+5000))},
|
||||
},
|
||||
target: ".",
|
||||
want: TestDir{
|
||||
"targetdir": TestDir{
|
||||
"targetfile": TestFile{Content: string(rtest.Random(888, 2*1024*1024+5000))},
|
||||
"targetfile": TestFile{Content: string(rtest.Random(888, 20*1024+5000))},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -769,8 +759,8 @@ func TestArchiverSaveDir(t *testing.T) {
|
||||
"foo": TestFile{Content: "foo"},
|
||||
"emptyfile": TestFile{Content: ""},
|
||||
"bar": TestFile{Content: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
|
||||
"largefile": TestFile{Content: string(rtest.Random(888, 2*1024*1024+5000))},
|
||||
"largerfile": TestFile{Content: string(rtest.Random(234, 5*1024*1024+5000))},
|
||||
"largefile": TestFile{Content: string(rtest.Random(888, 1*1024*1024+5000))},
|
||||
"largerfile": TestFile{Content: string(rtest.Random(234, 3*1024*1024+5000))},
|
||||
},
|
||||
},
|
||||
target: "targetdir",
|
||||
@@ -837,7 +827,7 @@ func TestArchiverSaveDir(t *testing.T) {
|
||||
t.Run("", func(t *testing.T) {
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||
|
||||
testFS := fs.Track{FS: fs.Local{}}
|
||||
testFS := fs.Track{FS: fs.NewLocal()}
|
||||
arch := New(repo, testFS, Options{})
|
||||
arch.summary = &Summary{}
|
||||
|
||||
@@ -931,7 +921,7 @@ func TestArchiverSaveDirDuplicateExcludedEntry(t *testing.T) {
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, src)
|
||||
|
||||
testFS := fs.Track{FS: &duplicateReaddirFS{
|
||||
FS: &fs.Local{},
|
||||
FS: fs.NewLocal(),
|
||||
dir: ".",
|
||||
names: []string{"excluded", "excluded", "keep"},
|
||||
}}
|
||||
@@ -990,8 +980,8 @@ func TestArchiverSaveDirIncremental(t *testing.T) {
|
||||
|
||||
// save the empty directory several times in a row, then have a look if the
|
||||
// archiver did save the same tree several times
|
||||
for i := 0; i < 5; i++ {
|
||||
testFS := fs.Track{FS: fs.Local{}}
|
||||
for i := range 5 {
|
||||
testFS := fs.Track{FS: fs.NewLocal()}
|
||||
arch := New(repo, testFS, Options{})
|
||||
arch.summary = &Summary{}
|
||||
|
||||
@@ -1173,7 +1163,7 @@ func TestArchiverSaveTree(t *testing.T) {
|
||||
t.Run("", func(t *testing.T) {
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||
|
||||
testFS := fs.Track{FS: fs.Local{}}
|
||||
testFS := fs.Track{FS: fs.NewLocal()}
|
||||
|
||||
arch := New(repo, testFS, Options{})
|
||||
arch.summary = &Summary{}
|
||||
@@ -1467,12 +1457,11 @@ func TestArchiverSnapshot(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||
|
||||
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
|
||||
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
|
||||
|
||||
chdir := tempdir
|
||||
if test.chdir != "" {
|
||||
@@ -1571,7 +1560,7 @@ func TestResolveRelativeTargetsSpecial(t *testing.T) {
|
||||
t.Skip("skip test on unix")
|
||||
}
|
||||
|
||||
targets, err := resolveRelativeTargets(&fs.Local{}, test.targets)
|
||||
targets, err := resolveRelativeTargets(fs.NewLocal(), test.targets)
|
||||
rtest.OK(t, err)
|
||||
paths := make([]string, len(targets))
|
||||
for i, tgt := range targets {
|
||||
@@ -1688,12 +1677,11 @@ func TestArchiverSnapshotSelect(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||
|
||||
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
|
||||
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
|
||||
arch.Select = test.selFn
|
||||
|
||||
back := rtest.Chdir(t, tempdir)
|
||||
@@ -1796,12 +1784,11 @@ func TestArchiverExplicitBackupTarget(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||
|
||||
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
|
||||
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
|
||||
arch.Select = test.selFn
|
||||
|
||||
back := rtest.Chdir(t, tempdir)
|
||||
@@ -1962,13 +1949,12 @@ func TestArchiverParent(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||
|
||||
testFS := &MockFS{
|
||||
FS: fs.Track{FS: fs.Local{}},
|
||||
FS: fs.Track{FS: fs.NewLocal()},
|
||||
bytesRead: make(map[string]int),
|
||||
}
|
||||
|
||||
@@ -1988,7 +1974,7 @@ func TestArchiverParent(t *testing.T) {
|
||||
t.Logf("testfs: %v", testFS)
|
||||
|
||||
// check that all files have been read exactly once
|
||||
TestWalkFiles(t, ".", test.src, func(filename string, item interface{}) error {
|
||||
TestWalkFiles(t, ".", test.src, func(filename string, item any) error {
|
||||
file, ok := item.(TestFile)
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -2150,8 +2136,7 @@ func TestArchiverErrorReporting(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||
|
||||
@@ -2162,7 +2147,7 @@ func TestArchiverErrorReporting(t *testing.T) {
|
||||
test.prepare(t)
|
||||
}
|
||||
|
||||
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
|
||||
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
|
||||
arch.Error = test.errFn
|
||||
|
||||
target := test.targets
|
||||
@@ -2243,7 +2228,7 @@ func TestArchiverContextCanceled(t *testing.T) {
|
||||
back := rtest.Chdir(t, tempdir)
|
||||
defer back()
|
||||
|
||||
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{})
|
||||
arch := New(repo, fs.Track{FS: fs.NewLocal()}, Options{})
|
||||
|
||||
_, snapshotID, _, err := arch.Snapshot(ctx, []string{"."}, SnapshotOptions{Time: time.Now()})
|
||||
|
||||
@@ -2376,8 +2361,7 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||
|
||||
@@ -2385,7 +2369,7 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
|
||||
defer back()
|
||||
|
||||
testFS := &TrackFS{
|
||||
FS: fs.Track{FS: fs.Local{}},
|
||||
FS: fs.Track{FS: fs.NewLocal()},
|
||||
opened: make(map[string]uint),
|
||||
}
|
||||
|
||||
@@ -2518,7 +2502,7 @@ func TestMetadataChanged(t *testing.T) {
|
||||
|
||||
// get metadata
|
||||
fi := lstat(t, "testfile")
|
||||
localFS := &fs.Local{}
|
||||
localFS := fs.NewLocal()
|
||||
meta, err := localFS.OpenFile("testfile", fs.O_NOFOLLOW, true)
|
||||
rtest.OK(t, err)
|
||||
want, err := meta.ToNode(false, t.Logf)
|
||||
@@ -2619,13 +2603,12 @@ func TestRacyFileTypeSwap(t *testing.T) {
|
||||
tempfile := filepath.Join(tempdir, realName)
|
||||
|
||||
statfs := &overrideFS{
|
||||
FS: fs.Local{},
|
||||
FS: fs.NewLocal(),
|
||||
overrideFI: fakeFI,
|
||||
resetFIOnRead: true,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
_ = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||
wg, ctx := errgroup.WithContext(ctx)
|
||||
@@ -2666,7 +2649,7 @@ func TestMetadataBackupErrorFiltering(t *testing.T) {
|
||||
filename := filepath.Join(tempdir, "file")
|
||||
repo := repository.TestRepository(t)
|
||||
|
||||
arch := New(repo, fs.Local{}, Options{})
|
||||
arch := New(repo, fs.NewLocal(), Options{})
|
||||
|
||||
var filteredErr error
|
||||
replacementErr := fmt.Errorf("replacement")
|
||||
@@ -2715,7 +2698,7 @@ func TestIrregularFile(t *testing.T) {
|
||||
fi.Mode = (fi.Mode &^ os.ModeType) | os.ModeIrregular
|
||||
|
||||
override := &overrideFS{
|
||||
FS: fs.Local{},
|
||||
FS: fs.NewLocal(),
|
||||
overrideFI: fi,
|
||||
overrideNode: &data.Node{
|
||||
Type: data.NodeTypeIrregular,
|
||||
@@ -2723,8 +2706,7 @@ func TestIrregularFile(t *testing.T) {
|
||||
overrideErr: fmt.Errorf(`unsupported file type "irregular"`),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
arch := New(repo, fs.Track{FS: override}, Options{})
|
||||
_, excluded, err := arch.save(ctx, "/", tempfile, nil, false)
|
||||
@@ -2770,13 +2752,12 @@ func TestDisappearedFile(t *testing.T) {
|
||||
back := rtest.Chdir(t, tempdir)
|
||||
defer back()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
// depending on the underlying FS implementation a missing file may be detected by OpenFile or
|
||||
// the subsequent file.Stat() call. Thus test both cases.
|
||||
for _, errorOnOpen := range []bool{false, true} {
|
||||
arch := New(repo, fs.Track{FS: &missingFS{FS: &fs.Local{}, errorOnOpen: errorOnOpen}}, Options{})
|
||||
arch := New(repo, fs.Track{FS: &missingFS{FS: fs.NewLocal(), errorOnOpen: errorOnOpen}}, Options{})
|
||||
_, excluded, err := arch.save(ctx, "/", filepath.Join(tempdir, "testdir"), nil, false)
|
||||
rtest.OK(t, err)
|
||||
rtest.Assert(t, excluded, "testfile should have been excluded")
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
func statAndSnapshot(t *testing.T, repo archiverRepo, name string) (*data.Node, *data.Node) {
|
||||
want := nodeFromFile(t, &fs.Local{}, name)
|
||||
_, node := snapshot(t, repo, &fs.Local{}, nil, name)
|
||||
want := nodeFromFile(t, fs.NewLocal(), name)
|
||||
_, node := snapshot(t, repo, fs.NewLocal(), nil, name)
|
||||
return want, node
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
func TestBufferPoolReuse(t *testing.T) {
|
||||
success := false
|
||||
// retries to avoid flakiness. The test can fail depending on the GC.
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
// Test that buffers are actually reused from the pool
|
||||
pool := newBufferPool(1024)
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestBufferPoolReuse(t *testing.T) {
|
||||
func TestBufferPoolLargeBuffers(t *testing.T) {
|
||||
success := false
|
||||
// retries to avoid flakiness. The test can fail depending on the GC.
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
// Test that buffers larger than defaultSize are not returned to pool
|
||||
pool := newBufferPool(1024)
|
||||
buf := pool.Get()
|
||||
|
||||
@@ -88,7 +88,7 @@ func (rc *rejectionCache) Store(dir string, rejected bool) {
|
||||
// non-nil if the filename component of excludeFileSpec is empty. If rc is
|
||||
// non-nil, it is going to be used in the RejectByNameFunc to expedite the evaluation
|
||||
// of a directory based on previous visits.
|
||||
func RejectIfPresent(excludeFileSpec string, warnf func(msg string, args ...interface{})) (RejectFunc, error) {
|
||||
func RejectIfPresent(excludeFileSpec string, warnf func(msg string, args ...any)) (RejectFunc, error) {
|
||||
if excludeFileSpec == "" {
|
||||
return nil, errors.New("name for exclusion tagfile is empty")
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func RejectIfPresent(excludeFileSpec string, warnf func(msg string, args ...inte
|
||||
// tagfile which bears the name specified in tagFilename and starts with
|
||||
// header. If rc is non-nil, it is used to expedite the evaluation of a
|
||||
// directory based on previous visits.
|
||||
func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache, fs fs.FS, warnf func(msg string, args ...interface{})) bool {
|
||||
func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache, fs fs.FS, warnf func(msg string, args ...any)) bool {
|
||||
if tagFilename == "" {
|
||||
return false
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache,
|
||||
return rejected
|
||||
}
|
||||
|
||||
func isDirExcludedByFile(dir, tagFilename, header string, fsInst fs.FS, warnf func(msg string, args ...interface{})) bool {
|
||||
func isDirExcludedByFile(dir, tagFilename, header string, fsInst fs.FS, warnf func(msg string, args ...any)) bool {
|
||||
tf := fsInst.Join(dir, tagFilename)
|
||||
_, err := fsInst.Lstat(tf)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -318,7 +318,7 @@ func RejectBySize(maxSize int64) (RejectFunc, error) {
|
||||
}
|
||||
|
||||
// RejectCloudFiles returns a func which rejects files which are online-only cloud files
|
||||
func RejectCloudFiles(warnf func(msg string, args ...interface{})) (RejectFunc, error) {
|
||||
func RejectCloudFiles(warnf func(msg string, args ...any)) (RejectFunc, error) {
|
||||
return func(item string, fi *fs.ExtendedFileInfo, _ fs.FS) bool {
|
||||
recall, err := fi.RecallOnDataAccess()
|
||||
if err != nil {
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestIsExcludedByFile(t *testing.T) {
|
||||
if tc.content == "" {
|
||||
h = ""
|
||||
}
|
||||
if got := isExcludedByFile(foo, tagFilename, h, newRejectionCache(), &fs.Local{}, func(msg string, args ...interface{}) { t.Logf(msg, args...) }); tc.want != got {
|
||||
if got := isExcludedByFile(foo, tagFilename, h, newRejectionCache(), fs.NewLocal(), func(msg string, args ...any) { t.Logf(msg, args...) }); tc.want != got {
|
||||
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
@@ -111,8 +111,8 @@ func TestMultipleIsExcludedByFile(t *testing.T) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
excludedByFoo := fooExclude(p, nil, &fs.Local{})
|
||||
excludedByBar := barExclude(p, nil, &fs.Local{})
|
||||
excludedByFoo := fooExclude(p, nil, fs.NewLocal())
|
||||
excludedByBar := barExclude(p, nil, fs.NewLocal())
|
||||
excluded := excludedByFoo || excludedByBar
|
||||
// the log message helps debugging in case the test fails
|
||||
t.Logf("%q: %v || %v = %v", p, excludedByFoo, excludedByBar, excluded)
|
||||
@@ -243,7 +243,7 @@ func TestDeviceMap(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
res, err := deviceMap.IsAllowed(filepath.FromSlash(test.item), test.deviceID, &fs.Local{})
|
||||
res, err := deviceMap.IsAllowed(filepath.FromSlash(test.item), test.deviceID, fs.NewLocal())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/restic/chunker"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
@@ -15,36 +14,38 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const chunkReadBufSize = 512 * 1024 // matches chunker internal read buffer size
|
||||
|
||||
// fileSaver concurrently saves incoming files to the repo.
|
||||
type fileSaver struct {
|
||||
saveFilePool *bufferPool
|
||||
uploader restic.BlobSaverAsync
|
||||
|
||||
pol chunker.Pol
|
||||
chunkerFactory restic.ChunkerFactory
|
||||
|
||||
ch chan<- saveFileJob
|
||||
|
||||
CompleteBlob func(bytes uint64)
|
||||
|
||||
NodeFromFileInfo func(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*data.Node, error)
|
||||
NodeFromFileInfo func(snPath, filename string, meta toNoder, ignoreXattrListError bool) (*data.Node, error)
|
||||
}
|
||||
|
||||
// newFileSaver returns a new file saver. A worker pool with fileWorkers is
|
||||
// started, it is stopped when ctx is cancelled.
|
||||
func newFileSaver(ctx context.Context, wg *errgroup.Group, uploader restic.BlobSaverAsync, pol chunker.Pol, fileWorkers uint) *fileSaver {
|
||||
func newFileSaver(ctx context.Context, wg *errgroup.Group, uploader restic.BlobSaverAsync, chunkerFactory restic.ChunkerFactory, fileWorkers uint) *fileSaver {
|
||||
ch := make(chan saveFileJob)
|
||||
debug.Log("new file saver with %v file workers", fileWorkers)
|
||||
|
||||
s := &fileSaver{
|
||||
uploader: uploader,
|
||||
saveFilePool: newBufferPool(chunker.MaxSize),
|
||||
pol: pol,
|
||||
ch: ch,
|
||||
uploader: uploader,
|
||||
saveFilePool: newBufferPool(chunkerFactory.MaxChunkSize()),
|
||||
chunkerFactory: chunkerFactory,
|
||||
ch: ch,
|
||||
|
||||
CompleteBlob: func(uint64) {},
|
||||
}
|
||||
|
||||
for i := uint(0); i < fileWorkers; i++ {
|
||||
for range fileWorkers {
|
||||
wg.Go(func() error {
|
||||
s.worker(ctx, ch)
|
||||
return nil
|
||||
@@ -100,8 +101,63 @@ type saveFileJob struct {
|
||||
complete fileCompleteFunc
|
||||
}
|
||||
|
||||
type fileChunkState struct {
|
||||
readBuf []byte
|
||||
bpos uint
|
||||
bmax uint
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (s *fileChunkState) reset() {
|
||||
s.bpos = 0
|
||||
s.bmax = 0
|
||||
s.closed = false
|
||||
}
|
||||
|
||||
// readNextChunk reads from rd and returns the next chunk of data. io.EOF is
|
||||
// returned when all chunks have been read.
|
||||
func (s *fileChunkState) readNextChunk(rd io.Reader, chnker restic.Chunker, data []byte) ([]byte, error) {
|
||||
data = data[:0]
|
||||
for {
|
||||
if s.bpos >= s.bmax {
|
||||
n, err := io.ReadFull(rd, s.readBuf)
|
||||
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
err = nil
|
||||
}
|
||||
|
||||
// io.EOF only happens when the end of the file has been reached.
|
||||
// If this is the case, we need to return the data we have read so far.
|
||||
if err == io.EOF && !s.closed {
|
||||
s.closed = true
|
||||
|
||||
if len(data) > 0 {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.bpos = 0
|
||||
s.bmax = uint(n)
|
||||
}
|
||||
|
||||
split := chnker.NextSplitPoint(s.readBuf[s.bpos:s.bmax])
|
||||
if split == -1 {
|
||||
data = append(data, s.readBuf[s.bpos:s.bmax]...)
|
||||
s.bpos = s.bmax
|
||||
} else {
|
||||
data = append(data, s.readBuf[s.bpos:s.bpos+uint(split)]...)
|
||||
s.bpos += uint(split)
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveFile stores the file f in the repo, then closes it.
|
||||
func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPath string, target string, f fs.File, start func(), finishReading func(), finish func(res futureNodeResult)) {
|
||||
func (s *fileSaver) saveFile(ctx context.Context, chnker restic.Chunker, chunkState *fileChunkState, snPath string, target string, f fs.File, start func(), finishReading func(), finish func(res futureNodeResult)) {
|
||||
start()
|
||||
|
||||
fnr := futureNodeResult{
|
||||
@@ -161,15 +217,15 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
||||
return
|
||||
}
|
||||
|
||||
// reuse the chunker
|
||||
chnker.Reset(f, s.pol)
|
||||
chnker.Reset()
|
||||
chunkState.reset()
|
||||
|
||||
node.Content = []restic.ID{}
|
||||
node.Size = 0
|
||||
var idx int
|
||||
for {
|
||||
buf := s.saveFilePool.Get()
|
||||
chunk, err := chnker.Next(buf.Data)
|
||||
chunkData, err := chunkState.readNextChunk(f, chnker, buf.Data)
|
||||
if err == io.EOF {
|
||||
buf.Release()
|
||||
break
|
||||
@@ -181,8 +237,9 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
||||
return
|
||||
}
|
||||
|
||||
buf.Data = chunk.Data
|
||||
node.Size += uint64(chunk.Length)
|
||||
// put result buffer back for later reuse
|
||||
buf.Data = chunkData
|
||||
node.Size += uint64(len(chunkData))
|
||||
|
||||
// test if the context has been cancelled, return the error
|
||||
if ctx.Err() != nil {
|
||||
@@ -199,7 +256,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
||||
node.Content = append(node.Content, restic.ID{})
|
||||
lock.Unlock()
|
||||
|
||||
s.uploader.SaveBlobAsync(ctx, restic.DataBlob, buf.Data, restic.ID{}, false, func(newID restic.ID, known bool, sizeInRepo int, err error) {
|
||||
s.uploader.SaveBlobAsync(ctx, restic.DataBlob, chunkData, restic.ID{}, false, func(newID restic.ID, known bool, sizeInRepo int, err error) {
|
||||
defer buf.Release()
|
||||
if err != nil {
|
||||
completeError(err)
|
||||
@@ -209,7 +266,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
||||
lock.Lock()
|
||||
if !known {
|
||||
fnr.stats.DataBlobs++
|
||||
fnr.stats.DataSize += uint64(len(buf.Data))
|
||||
fnr.stats.DataSize += uint64(len(chunkData))
|
||||
fnr.stats.DataSizeInRepo += uint64(sizeInRepo)
|
||||
}
|
||||
node.Content[pos] = newID
|
||||
@@ -226,7 +283,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
||||
return
|
||||
}
|
||||
|
||||
s.CompleteBlob(uint64(len(chunk.Data)))
|
||||
s.CompleteBlob(uint64(len(chunkData)))
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
@@ -246,8 +303,8 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
||||
}
|
||||
|
||||
func (s *fileSaver) worker(ctx context.Context, jobs <-chan saveFileJob) {
|
||||
// a worker has one chunker which is reused for each file (because it contains a rather large buffer)
|
||||
chnker := chunker.New(nil, s.pol)
|
||||
chnker := s.chunkerFactory.NewChunker()
|
||||
chunkState := &fileChunkState{readBuf: make([]byte, chunkReadBufSize)}
|
||||
|
||||
for {
|
||||
var job saveFileJob
|
||||
@@ -261,7 +318,7 @@ func (s *fileSaver) worker(ctx context.Context, jobs <-chan saveFileJob) {
|
||||
}
|
||||
}
|
||||
|
||||
s.saveFile(ctx, chnker, job.snPath, job.target, job.file, job.start, func() {
|
||||
s.saveFile(ctx, chnker, chunkState, job.snPath, job.target, job.file, job.start, func() {
|
||||
if job.completeReading != nil {
|
||||
job.completeReading()
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/chunker"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/test"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func createTestFiles(t testing.TB, num int) (files []string) {
|
||||
tempdir := test.TempDir(t)
|
||||
|
||||
for i := 0; i < num; i++ {
|
||||
for i := range num {
|
||||
filename := fmt.Sprintf("testfile-%d", i)
|
||||
err := os.WriteFile(filepath.Join(tempdir, filename), []byte(filename), 0600)
|
||||
if err != nil {
|
||||
@@ -34,14 +34,11 @@ func startFileSaver(ctx context.Context, t testing.TB, _ fs.FS) (*fileSaver, *mo
|
||||
wg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
workers := uint(runtime.NumCPU())
|
||||
pol, err := chunker.RandomPolynomial()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
chunkerFactory := repository.TestRepository(t).ChunkerFactory()
|
||||
|
||||
saver := &mockSaver{saved: make(map[string]int)}
|
||||
s := newFileSaver(ctx, wg, saver, pol, workers)
|
||||
s.NodeFromFileInfo = func(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*data.Node, error) {
|
||||
s := newFileSaver(ctx, wg, saver, chunkerFactory, workers)
|
||||
s.NodeFromFileInfo = func(snPath, filename string, meta toNoder, ignoreXattrListError bool) (*data.Node, error) {
|
||||
return meta.ToNode(ignoreXattrListError, t.Logf)
|
||||
}
|
||||
|
||||
@@ -49,15 +46,14 @@ func startFileSaver(ctx context.Context, t testing.TB, _ fs.FS) (*fileSaver, *mo
|
||||
}
|
||||
|
||||
func TestFileSaver(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
startFn := func() {}
|
||||
completeReadingFn := func() {}
|
||||
completeFn := func(*data.Node, ItemStats) {}
|
||||
|
||||
files := createTestFiles(t, 15)
|
||||
testFs := fs.Local{}
|
||||
testFs := fs.NewLocal()
|
||||
s, saver, ctx, wg := startFileSaver(ctx, t, testFs)
|
||||
|
||||
var results []futureNode
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package archiver
|
||||
|
||||
import "github.com/restic/restic/internal/data"
|
||||
|
||||
// ItemAction describes backup progress for a single file or directory.
|
||||
// The zero value indicates an error or incomplete item.
|
||||
type ItemAction string
|
||||
|
||||
// Constants for the different CompleteItem actions.
|
||||
const (
|
||||
ActionDirNew ItemAction = "dir new"
|
||||
ActionDirUnchanged ItemAction = "dir unchanged"
|
||||
ActionDirModified ItemAction = "dir modified"
|
||||
ActionFileNew ItemAction = "file new"
|
||||
ActionFileUnchanged ItemAction = "file unchanged"
|
||||
ActionFileModified ItemAction = "file modified"
|
||||
)
|
||||
|
||||
// IsZero reports whether the action describes a zero value.
|
||||
func (a ItemAction) IsZero() bool {
|
||||
return a == ""
|
||||
}
|
||||
|
||||
func itemProgressAction(previous, current *data.Node) ItemAction {
|
||||
if current == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch current.Type {
|
||||
case data.NodeTypeDir:
|
||||
switch {
|
||||
case previous == nil:
|
||||
return ActionDirNew
|
||||
case previous.Equals(*current):
|
||||
return ActionDirUnchanged
|
||||
default:
|
||||
return ActionDirModified
|
||||
}
|
||||
|
||||
case data.NodeTypeFile:
|
||||
switch {
|
||||
case previous == nil:
|
||||
return ActionFileNew
|
||||
case previous.Equals(*current):
|
||||
return ActionFileUnchanged
|
||||
default:
|
||||
return ActionFileModified
|
||||
}
|
||||
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -78,8 +78,7 @@ func TestScanner(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir := rtest.TempDir(t)
|
||||
TestCreateFiles(t, tempdir, test.src)
|
||||
@@ -92,7 +91,7 @@ func TestScanner(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sc := NewScanner(fs.Track{FS: fs.Local{}})
|
||||
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
|
||||
if test.selFn != nil {
|
||||
sc.Select = test.selFn
|
||||
}
|
||||
@@ -213,8 +212,7 @@ func TestScannerError(t *testing.T) {
|
||||
t.Skipf("skip on windows")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir := rtest.TempDir(t)
|
||||
TestCreateFiles(t, tempdir, test.src)
|
||||
@@ -231,7 +229,7 @@ func TestScannerError(t *testing.T) {
|
||||
test.prepare(t)
|
||||
}
|
||||
|
||||
sc := NewScanner(fs.Track{FS: fs.Local{}})
|
||||
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
|
||||
if test.selFn != nil {
|
||||
sc.Select = test.selFn
|
||||
}
|
||||
@@ -299,7 +297,7 @@ func TestScannerCancel(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sc := NewScanner(fs.Track{FS: fs.Local{}})
|
||||
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
|
||||
var lastStats ScanStats
|
||||
sc.Result = func(item string, s ScanStats) {
|
||||
lastStats = s
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
// TestSnapshot creates a new snapshot of path.
|
||||
func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *restic.ID) *data.Snapshot {
|
||||
arch := New(repo, fs.Local{}, Options{})
|
||||
arch := New(repo, fs.NewLocal(), Options{})
|
||||
opts := SnapshotOptions{
|
||||
Time: time.Now(),
|
||||
Hostname: "localhost",
|
||||
@@ -41,7 +41,7 @@ func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *res
|
||||
}
|
||||
|
||||
// TestDir describes a directory structure to create for a test.
|
||||
type TestDir map[string]interface{}
|
||||
type TestDir map[string]any
|
||||
|
||||
func (d TestDir) String() string {
|
||||
return "<Dir>"
|
||||
@@ -119,7 +119,7 @@ func TestCreateFiles(t testing.TB, target string, dir TestDir) {
|
||||
|
||||
// TestWalkFunc is used by TestWalkFiles to traverse the dir. When an error is
|
||||
// returned, traversal stops and the surrounding test is marked as failed.
|
||||
type TestWalkFunc func(path string, item interface{}) error
|
||||
type TestWalkFunc func(path string, item any) error
|
||||
|
||||
// TestWalkFiles runs fn for each file/directory in dir, the filename will be
|
||||
// constructed with target as the prefix. Symlinks on Windows are ignored.
|
||||
@@ -158,7 +158,7 @@ func TestEnsureFiles(t testing.TB, target string, dir TestDir) {
|
||||
pathsChecked := make(map[string]struct{})
|
||||
|
||||
// first, test that all items are there
|
||||
TestWalkFiles(t, target, dir, func(path string, item interface{}) error {
|
||||
TestWalkFiles(t, target, dir, func(path string, item any) error {
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -243,7 +243,7 @@ func TestEnsureFileContent(ctx context.Context, t testing.TB, repo restic.BlobLo
|
||||
content := make([]byte, len(file.Content))
|
||||
pos := 0
|
||||
for _, id := range node.Content {
|
||||
part, err := repo.LoadBlob(ctx, restic.DataBlob, id, content[pos:])
|
||||
part, err := repo.LoadBlob(ctx, restic.BlobHandle{Type: restic.DataBlob, ID: id}, content[pos:])
|
||||
if err != nil {
|
||||
t.Fatalf("error loading blob %v: %v", id.Str(), err)
|
||||
return
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package archiver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -28,30 +27,30 @@ func (t *MockT) Fail() {
|
||||
}
|
||||
|
||||
// Fatal is equivalent to Log followed by FailNow.
|
||||
func (t *MockT) Fatal(args ...interface{}) {
|
||||
func (t *MockT) Fatal(args ...any) {
|
||||
t.T.Logf("MockT Fatal called with %v", args)
|
||||
t.HasFailed = true
|
||||
}
|
||||
|
||||
// Fatalf is equivalent to Logf followed by FailNow.
|
||||
func (t *MockT) Fatalf(msg string, args ...interface{}) {
|
||||
func (t *MockT) Fatalf(msg string, args ...any) {
|
||||
t.T.Logf("MockT Fatal called: "+msg, args...)
|
||||
t.HasFailed = true
|
||||
}
|
||||
|
||||
// Error is equivalent to Log followed by Fail.
|
||||
func (t *MockT) Error(args ...interface{}) {
|
||||
func (t *MockT) Error(args ...any) {
|
||||
t.T.Logf("MockT Error called with %v", args)
|
||||
t.HasFailed = true
|
||||
}
|
||||
|
||||
// Errorf is equivalent to Logf followed by Fail.
|
||||
func (t *MockT) Errorf(msg string, args ...interface{}) {
|
||||
func (t *MockT) Errorf(msg string, args ...any) {
|
||||
t.T.Logf("MockT Error called: "+msg, args...)
|
||||
t.HasFailed = true
|
||||
}
|
||||
|
||||
func createFilesAt(t testing.TB, targetdir string, files map[string]interface{}) {
|
||||
func createFilesAt(t testing.TB, targetdir string, files map[string]any) {
|
||||
for name, item := range files {
|
||||
target := filepath.Join(targetdir, filepath.FromSlash(name))
|
||||
err := os.MkdirAll(filepath.Dir(target), 0700)
|
||||
@@ -77,7 +76,7 @@ func createFilesAt(t testing.TB, targetdir string, files map[string]interface{})
|
||||
func TestTestCreateFiles(t *testing.T) {
|
||||
var tests = []struct {
|
||||
dir TestDir
|
||||
files map[string]interface{}
|
||||
files map[string]any
|
||||
}{
|
||||
{
|
||||
dir: TestDir{
|
||||
@@ -91,7 +90,7 @@ func TestTestCreateFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
"subdir": TestDir{},
|
||||
"subdir/subfile": TestFile{Content: "bar"},
|
||||
@@ -196,7 +195,7 @@ func TestTestWalkFiles(t *testing.T) {
|
||||
got := make(map[string]string)
|
||||
|
||||
TestCreateFiles(t, tempdir, test.dir)
|
||||
TestWalkFiles(t, tempdir, test.dir, func(path string, item interface{}) error {
|
||||
TestWalkFiles(t, tempdir, test.dir, func(path string, item any) error {
|
||||
p, err := filepath.Rel(tempdir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -216,11 +215,11 @@ func TestTestWalkFiles(t *testing.T) {
|
||||
func TestTestEnsureFiles(t *testing.T) {
|
||||
var tests = []struct {
|
||||
expectFailure bool
|
||||
files map[string]interface{}
|
||||
files map[string]any
|
||||
want TestDir
|
||||
}{
|
||||
{
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
"subdir/subfile": TestFile{Content: "bar"},
|
||||
"x/y/link": TestSymlink{Target: filepath.Clean("../../foo")},
|
||||
@@ -239,7 +238,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -251,7 +250,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
"subdir/subfile": TestFile{Content: "bar"},
|
||||
},
|
||||
@@ -261,7 +260,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "xxx"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -270,7 +269,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestSymlink{Target: "/xxx"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -279,7 +278,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -288,7 +287,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestSymlink{Target: "xxx"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -297,7 +296,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestDir{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
},
|
||||
@@ -308,7 +307,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -341,11 +340,11 @@ func TestTestEnsureFiles(t *testing.T) {
|
||||
func TestTestEnsureSnapshot(t *testing.T) {
|
||||
var tests = []struct {
|
||||
expectFailure bool
|
||||
files map[string]interface{}
|
||||
files map[string]any
|
||||
want TestDir
|
||||
}{
|
||||
{
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
filepath.FromSlash("subdir/subfile"): TestFile{Content: "bar"},
|
||||
filepath.FromSlash("x/y/link"): TestSymlink{Target: filepath.FromSlash("../../foo")},
|
||||
@@ -366,7 +365,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -377,7 +376,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
"bar": TestFile{Content: "bar"},
|
||||
},
|
||||
@@ -389,7 +388,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -401,7 +400,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -414,7 +413,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -425,7 +424,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -436,7 +435,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
expectFailure: true,
|
||||
files: map[string]interface{}{
|
||||
files: map[string]any{
|
||||
"foo": TestFile{Content: "foo"},
|
||||
},
|
||||
want: TestDir{
|
||||
@@ -449,8 +448,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
tempdir := rtest.TempDir(t)
|
||||
|
||||
@@ -467,7 +465,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
||||
|
||||
repo := repository.TestRepository(t)
|
||||
|
||||
arch := New(repo, fs.Local{}, Options{})
|
||||
arch := New(repo, fs.NewLocal(), Options{})
|
||||
opts := SnapshotOptions{
|
||||
Time: time.Now(),
|
||||
Hostname: "localhost",
|
||||
|
||||
@@ -274,16 +274,16 @@ func unrollTree(f fs.FS, t *tree) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupTarget is a resolved backup path and whether the user passed this path
|
||||
// backupTarget is a resolved backup path and whether the user passed this path
|
||||
// literally. Paths inserted when expanding a target with no path components
|
||||
// (for example ".") have Explicit false so include/exclude rules still apply.
|
||||
type BackupTarget struct {
|
||||
type backupTarget struct {
|
||||
Path string
|
||||
Explicit bool
|
||||
}
|
||||
|
||||
// newTree creates a Tree from the target files/directories.
|
||||
func newTree(fs fs.FS, targets []BackupTarget) (*tree, error) {
|
||||
func newTree(fs fs.FS, targets []backupTarget) (*tree, error) {
|
||||
debug.Log("targets: %v", targets)
|
||||
tree := &tree{}
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user