mirror of
https://github.com/restic/restic.git
synced 2026-09-14 16:27:59 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6335213501 | ||
|
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
- name: Codespell
|
- name: Codespell
|
||||||
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
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:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go ${{ matrix.go }}
|
- name: Set up Go ${{ matrix.go }}
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
@@ -222,10 +222,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go ${{ env.latest_go }}
|
- name: Set up Go ${{ env.latest_go }}
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.latest_go }}
|
go-version: ${{ env.latest_go }}
|
||||||
|
|
||||||
@@ -244,10 +244,10 @@ jobs:
|
|||||||
checks: write
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go ${{ env.latest_go }}
|
- name: Set up Go ${{ env.latest_go }}
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.latest_go }}
|
go-version: ${{ env.latest_go }}
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
|||||||
+7
-1
@@ -32,7 +32,6 @@ linters:
|
|||||||
backend-imports:
|
backend-imports:
|
||||||
files:
|
files:
|
||||||
- "**/internal/backend/**"
|
- "**/internal/backend/**"
|
||||||
- "!**/internal/backend/cache/**"
|
|
||||||
- "!**/internal/backend/test/**"
|
- "!**/internal/backend/test/**"
|
||||||
- "!**/*_test.go"
|
- "!**/*_test.go"
|
||||||
deny:
|
deny:
|
||||||
@@ -40,6 +39,13 @@ linters:
|
|||||||
desc: "internal/restic should not be imported to keep the architectural layers intact"
|
desc: "internal/restic should not be imported to keep the architectural layers intact"
|
||||||
- pkg: "github.com/restic/restic/internal/repository"
|
- pkg: "github.com/restic/restic/internal/repository"
|
||||||
desc: "internal/repository should not be imported to keep the architectural layers intact"
|
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:
|
importas:
|
||||||
alias:
|
alias:
|
||||||
- pkg: github.com/restic/restic/internal/test
|
- pkg: github.com/restic/restic/internal/test
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ release. Instructions on how to do that are contained in the
|
|||||||
|
|
||||||
## News
|
## 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/).
|
the [project blog](https://restic.net/blog/).
|
||||||
|
|
||||||
## License
|
## 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"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -53,22 +54,7 @@ Exit status is 11 if the repository is already locked.
|
|||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
PreRunE: func(_ *cobra.Command, _ []string) error {
|
PreRunE: func(_ *cobra.Command, _ []string) error {
|
||||||
if envVal := os.Getenv("RESTIC_READ_CONCURRENCY"); envVal != "" && !opts.readConcurrencyFlag.Changed {
|
return opts.Finalize()
|
||||||
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
|
|
||||||
},
|
},
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
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.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.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.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.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")
|
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.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")
|
f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
@@ -157,12 +143,35 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
|
|
||||||
opts.readConcurrencyFlag = f.Lookup("read-concurrency")
|
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
|
// parse host from env, if not exists or empty the default value will be used
|
||||||
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||||
opts.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
|
var backupFSTestHook func(fs fs.FS) fs.FS
|
||||||
|
|
||||||
// ErrInvalidSourceData is used to report an incomplete backup
|
// 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
|
// filterExisting returns the items that exist and can be accessed. It returns
|
||||||
// ErrNoSourceData if none remain, or ErrInvalidSourceData if some were skipped.
|
// 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 {
|
for _, item := range items {
|
||||||
_, err := fs.Lstat(item)
|
_, err := fs.Lstat(item)
|
||||||
if err != nil {
|
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...)
|
filesFrom := append(append(opts.FilesFrom, opts.FilesFromVerbatim...), opts.FilesFromRaw...)
|
||||||
for _, filename := range filesFrom {
|
if slices.Contains(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")
|
||||||
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
|
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
||||||
// from being saved in a snapshot based on path only
|
// from being saved in a snapshot based on path only
|
||||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, 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
|
// exclude restic cache
|
||||||
if repo.Cache() != nil {
|
if repo.Cache() != nil {
|
||||||
f, err := rejectResticCache(repo)
|
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
|
// collectRejectFuncs returns a list of all functions which may reject data
|
||||||
// from being saved in a snapshot based on path and file info
|
// from being saved in a snapshot based on path and file info
|
||||||
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, 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
|
// allowed devices
|
||||||
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
|
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
|
||||||
f, err := archiver.RejectByDevice(targets, fs)
|
f, err := archiver.RejectByDevice(targets, fs)
|
||||||
@@ -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.
|
// 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 {
|
if opts.Stdin || opts.StdinCommand {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -537,8 +544,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
|
|||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
progressReporter := backup.NewProgress(printer,
|
progressReporter := backup.NewProgress(printer, gopts.Quiet, gopts.JSON, term.CanUpdateStatus())
|
||||||
ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
|
|
||||||
defer progressReporter.Done()
|
defer progressReporter.Done()
|
||||||
|
|
||||||
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
|
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
|
||||||
@@ -572,7 +578,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetFS fs.FS = fs.Local{}
|
targetFS := fs.NewLocal()
|
||||||
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
|
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
|
||||||
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
|
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -582,7 +588,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
|
|||||||
_ = progressReporter.Error(item, err)
|
_ = progressReporter.Error(item, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
messageHandler := func(msg string, args ...interface{}) {
|
messageHandler := func(msg string, args ...any) {
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
printer.P(msg, args...)
|
printer.P(msg, args...)
|
||||||
}
|
}
|
||||||
@@ -663,6 +669,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te
|
|||||||
arch.CompleteItem = progressReporter.CompleteItem
|
arch.CompleteItem = progressReporter.CompleteItem
|
||||||
arch.StartFile = progressReporter.StartFile
|
arch.StartFile = progressReporter.StartFile
|
||||||
arch.CompleteBlob = progressReporter.CompleteBlob
|
arch.CompleteBlob = progressReporter.CompleteBlob
|
||||||
|
arch.ExcludedItem = progressReporter.ExcludedItem
|
||||||
|
|
||||||
if opts.IgnoreInode {
|
if opts.IgnoreInode {
|
||||||
// --ignore-inode implies --ignore-ctime: on FUSE, the ctime is not
|
// --ignore-inode implies --ignore-ctime: on FUSE, the ctime is not
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,8 +17,10 @@ import (
|
|||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
"github.com/restic/restic/internal/ui/backup"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) error {
|
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) {
|
func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) {
|
||||||
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
|
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
|
||||||
rtest.Assert(t, err == nil, "Error while backing up: %v", err)
|
rtest.Assert(t, err == nil, "Error while backing up: %v", err)
|
||||||
@@ -49,6 +62,9 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
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)
|
testSetupBackupData(t, env)
|
||||||
opts := BackupOptions{UseFsSnapshot: useFsSnapshot}
|
opts := BackupOptions{UseFsSnapshot: useFsSnapshot}
|
||||||
|
|
||||||
@@ -139,7 +155,7 @@ func (f *vssDeleteOriginalFS) Lstat(name string) (*fs.ExtendedFileInfo, error) {
|
|||||||
_, _ = f.FS.Lstat(name)
|
_, _ = f.FS.Lstat(name)
|
||||||
// nuke testdata
|
// nuke testdata
|
||||||
var err error
|
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
|
// 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
|
// thus try a few times to remove the data
|
||||||
err = os.RemoveAll(f.testdata)
|
err = os.RemoveAll(f.testdata)
|
||||||
@@ -615,13 +631,7 @@ func linkEqual(source, dest []string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range source {
|
for i := range source {
|
||||||
found := false
|
found := slices.Contains(dest, source[i])
|
||||||
for j := range dest {
|
|
||||||
if source[i] == dest[j] {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
if !found {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -723,10 +733,47 @@ func TestBackupSkipIfUnchanged(t *testing.T) {
|
|||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
opts := BackupOptions{SkipIfUnchanged: true}
|
opts := BackupOptions{SkipIfUnchanged: true}
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for range 3 {
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
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/errors"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
"github.com/restic/restic/internal/ui/table"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"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 {
|
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 {
|
if len(args) > 0 {
|
||||||
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
|
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
|
||||||
|
|||||||
+8
-12
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
|
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")
|
return errors.Fatal("type not specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
validType := false
|
validType := slices.Contains(catAllowedCmds, args[0])
|
||||||
for _, v := range catAllowedCmds {
|
|
||||||
if v == args[0] {
|
|
||||||
validType = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !validType {
|
if !validType {
|
||||||
return errors.Fatalf("invalid type %q, must be one of [%s]", args[0], strings.Join(catAllowedCmds, "|"))
|
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 {
|
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 {
|
if err := validateCatArgs(args); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -141,7 +137,7 @@ func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Te
|
|||||||
printer.S(string(buf))
|
printer.S(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "lock":
|
case "lock":
|
||||||
lock, err := restic.LoadLock(ctx, repo, id)
|
lock, err := repository.LoadLock(ctx, repo, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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} {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := repo.LoadBlob(ctx, t, id, nil)
|
buf, err := repo.LoadBlob(ctx, restic.BlobHandle{Type: t, ID: id}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -207,7 +203,7 @@ func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Te
|
|||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
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 explicitly requested --no-cache, we don't use any cache
|
||||||
// - if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check
|
// - if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check
|
||||||
// - by default, we use a cache in a temporary directory that is deleted after the check
|
// - by default, we use a cache in a temporary directory that is deleted after the check
|
||||||
func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progress.Printer) (cleanup func()) {
|
func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer restic.Printer) (cleanup func()) {
|
||||||
cleanup = func() {}
|
cleanup = func() {}
|
||||||
if opts.WithCache {
|
if opts.WithCache {
|
||||||
// use the default cache, no setup needed
|
// use the default cache, no setup needed
|
||||||
@@ -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) {
|
func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args []string, term ui.Terminal) (checkSummary, error) {
|
||||||
summary := checkSummary{MessageType: "summary"}
|
summary := checkSummary{MessageType: "summary"}
|
||||||
|
|
||||||
var printer progress.Printer
|
var printer restic.Printer
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
printer = ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
printer = progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
} else {
|
} else {
|
||||||
printer = newJSONErrorPrinter(term)
|
printer = newJSONErrorPrinter(term)
|
||||||
}
|
}
|
||||||
@@ -307,13 +307,13 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
|||||||
go chkr.Packs(ctx, errChan)
|
go chkr.Packs(ctx, errChan)
|
||||||
|
|
||||||
for err := range errChan {
|
for err := range errChan {
|
||||||
var packErr *repository.PackError
|
var packErr *repository.ErrPackMetadata
|
||||||
if errors.As(err, &packErr) {
|
if errors.As(err, &packErr) {
|
||||||
if packErr.Orphaned {
|
if packErr.Orphaned {
|
||||||
orphanedPacks++
|
orphanedPacks++
|
||||||
printer.V("%v\n", err)
|
printer.V("%v\n", err)
|
||||||
} else {
|
} else {
|
||||||
if packErr.Truncated {
|
if packErr.Truncated || packErr.Missing {
|
||||||
salvagePacks.Insert(packErr.ID)
|
salvagePacks.Insert(packErr.ID)
|
||||||
}
|
}
|
||||||
errorsFound = true
|
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")
|
printer.P("check snapshots, trees and blobs\n")
|
||||||
errChan = make(chan error)
|
errChan = make(chan error)
|
||||||
|
var brokenSnapshots []string
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
bar := printer.NewCounter("snapshots")
|
bar := printer.NewCounter("snapshots")
|
||||||
defer bar.Done()
|
defer bar.Done()
|
||||||
chkr.Structure(ctx, bar, errChan)
|
chkr.Structure(ctx, bar, errChan)
|
||||||
}()
|
})
|
||||||
|
|
||||||
for err := range errChan {
|
for err := range errChan {
|
||||||
errorsFound = true
|
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())
|
printer.E("error for tree %v:\n", e.ID.Str())
|
||||||
for _, treeErr := range e.Errors {
|
for _, treeErr := range e.Errors {
|
||||||
summary.NumErrors++
|
summary.NumErrors++
|
||||||
printer.E(" %v\n", treeErr)
|
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++
|
summary.NumErrors++
|
||||||
printer.E("error: %v\n", err)
|
printer.E("error: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -389,10 +392,9 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
|||||||
}
|
}
|
||||||
|
|
||||||
if readDataFilter != nil {
|
if readDataFilter != nil {
|
||||||
p := printer.NewCounter("packs")
|
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
|
|
||||||
go chkr.ReadPacks(ctx, readDataFilter, p, errChan)
|
go chkr.ReadPacks(ctx, readDataFilter, printer, errChan)
|
||||||
|
|
||||||
for err := range errChan {
|
for err := range errChan {
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
@@ -402,7 +404,6 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
|
|||||||
salvagePacks.Insert(err.PackID)
|
salvagePacks.Insert(err.PackID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.Done()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(salvagePacks) > 0 {
|
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")
|
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 {
|
if ctx.Err() != nil {
|
||||||
return summary, ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorsFound {
|
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")
|
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")
|
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
|
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) {
|
filteredStatus bool) (func(packs map[restic.ID]int64) map[restic.ID]int64, error) {
|
||||||
typeData := ""
|
typeData := ""
|
||||||
if filteredStatus {
|
if filteredStatus {
|
||||||
@@ -553,23 +560,23 @@ func newJSONErrorPrinter(term ui.Terminal) *jsonErrorPrinter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter {
|
func (*jsonErrorPrinter) NewCounter(_ string) restic.Counter {
|
||||||
return nil
|
return restic.NoopCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) *progress.Counter {
|
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) restic.Counter {
|
||||||
return nil
|
return restic.NoopCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
func (p *jsonErrorPrinter) E(msg string, args ...any) {
|
||||||
status := checkError{
|
status := checkError{
|
||||||
MessageType: "error",
|
MessageType: "error",
|
||||||
Message: fmt.Sprintf(msg, args...),
|
Message: fmt.Sprintf(msg, args...),
|
||||||
}
|
}
|
||||||
p.term.Error(ui.ToJSONString(status))
|
p.term.Error(ui.ToJSONString(status))
|
||||||
}
|
}
|
||||||
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) S(_ string, _ ...any) {}
|
||||||
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) P(_ string, _ ...any) {}
|
||||||
func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) PT(_ string, _ ...any) {}
|
||||||
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) V(_ string, _ ...any) {}
|
||||||
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) VV(_ string, _ ...any) {}
|
||||||
|
|||||||
@@ -11,35 +11,36 @@ import (
|
|||||||
|
|
||||||
func testRunCheck(t testing.TB, gopts global.Options) {
|
func testRunCheck(t testing.TB, gopts global.Options) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
output, err := testRunCheckOutput(t, gopts, true)
|
stdout, stderr, err := testRunCheckOutput(t, gopts, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(output)
|
t.Error(stdout)
|
||||||
|
t.Error(stderr)
|
||||||
t.Fatalf("unexpected error: %+v", err)
|
t.Fatalf("unexpected error: %+v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunCheckMustFail(t testing.TB, gopts global.Options) {
|
func testRunCheckMustFail(t testing.TB, gopts global.Options) {
|
||||||
t.Helper()
|
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")
|
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) {
|
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, string, error) {
|
||||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
stdout, stderr, err := withCaptureStdoutStderr(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
opts := CheckOptions{
|
opts := CheckOptions{
|
||||||
ReadData: true,
|
ReadData: true,
|
||||||
CheckUnused: checkUnused,
|
CheckUnused: checkUnused,
|
||||||
}
|
}
|
||||||
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.Term)
|
_, err := runCheck(ctx, opts, gopts, nil, gopts.Term)
|
||||||
return err
|
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) {
|
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 {
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
gopts.Verbosity = 2
|
gopts.Verbosity = 2
|
||||||
_, err := runCheck(context.TODO(), opts, gopts, args, gopts.Term)
|
_, err := runCheck(ctx, opts, gopts, args, gopts.Term)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
return buf.String(), err
|
return buf.String(), err
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParsePercentage(t *testing.T) {
|
func TestParsePercentage(t *testing.T) {
|
||||||
@@ -204,7 +203,7 @@ func TestPrepareCheckCache(t *testing.T) {
|
|||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
gopts := global.Options{CacheDir: tmpDirBase}
|
gopts := global.Options{CacheDir: tmpDirBase}
|
||||||
cleanup := prepareCheckCache(testCase.opts, &gopts, &progress.NoopPrinter{})
|
cleanup := prepareCheckCache(testCase.opts, &gopts, restic.NewNoopPrinter())
|
||||||
files, err := os.ReadDir(tmpDirBase)
|
files, err := os.ReadDir(tmpDirBase)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
@@ -234,7 +233,7 @@ func TestPrepareCheckCache(t *testing.T) {
|
|||||||
|
|
||||||
func TestPrepareDefaultCheckCache(t *testing.T) {
|
func TestPrepareDefaultCheckCache(t *testing.T) {
|
||||||
gopts := global.Options{CacheDir: ""}
|
gopts := global.Options{CacheDir: ""}
|
||||||
cleanup := prepareCheckCache(CheckOptions{}, &gopts, &progress.NoopPrinter{})
|
cleanup := prepareCheckCache(CheckOptions{}, &gopts, restic.NewNoopPrinter())
|
||||||
_, err := os.ReadDir(gopts.CacheDir)
|
_, err := os.ReadDir(gopts.CacheDir)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
|
|||||||
+45
-26
@@ -72,14 +72,22 @@ func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errSentinelEndIteration = errors.New("end iteration")
|
||||||
|
|
||||||
// collectAllSnapshots: select all snapshot trees to be copied
|
// collectAllSnapshots: select all snapshot trees to be copied
|
||||||
func collectAllSnapshots(ctx context.Context, opts CopyOptions,
|
func collectAllSnapshots(ctx context.Context, opts CopyOptions,
|
||||||
srcSnapshotLister restic.Lister, srcRepo restic.Repository,
|
srcSnapshotLister restic.Lister, srcRepo restic.Repository,
|
||||||
dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer progress.Printer,
|
dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer restic.Printer,
|
||||||
) iter.Seq[*data.Snapshot] {
|
) iter.Seq2[*data.Snapshot, error] {
|
||||||
return func(yield func(*data.Snapshot) bool) {
|
return func(yield func(*data.Snapshot, error) bool) {
|
||||||
for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args, printer) {
|
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
|
// 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()
|
srcOriginal := *sn.ID()
|
||||||
if sn.Original != nil {
|
if sn.Original != nil {
|
||||||
srcOriginal = *sn.Original
|
srcOriginal = *sn.Original
|
||||||
@@ -95,18 +103,22 @@ func collectAllSnapshots(ctx context.Context, opts CopyOptions,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isCopy {
|
if isCopy {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !yield(sn) {
|
if !yield(sn, nil) {
|
||||||
return
|
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 {
|
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")
|
secondaryGopts, isFromRepo, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "destination")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -148,15 +160,19 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts global.Options, args [
|
|||||||
}
|
}
|
||||||
|
|
||||||
dstSnapshotByOriginal := make(map[restic.ID][]*data.Snapshot)
|
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() {
|
if sn.Original != nil && !sn.Original.IsNull() {
|
||||||
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
||||||
}
|
}
|
||||||
// also consider identical snapshot copies
|
// also consider identical snapshot copies
|
||||||
dstSnapshotByOriginal[*sn.ID()] = append(dstSnapshotByOriginal[*sn.ID()], sn)
|
dstSnapshotByOriginal[*sn.ID()] = append(dstSnapshotByOriginal[*sn.ID()], sn)
|
||||||
}
|
return nil
|
||||||
if ctx.Err() != nil {
|
})
|
||||||
return ctx.Err()
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedSnapshots := collectAllSnapshots(ctx, opts, srcSnapshotLister, srcRepo, dstSnapshotByOriginal, args, printer)
|
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
|
// copyTreeBatched copies multiple snapshots in one go. Snapshots are written after
|
||||||
// data equivalent to at least 10 packfiles was written.
|
// data equivalent to at least 10 packfiles was written.
|
||||||
func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
func copyTreeBatched(ctx context.Context, srcRepo *repository.Repository, dstRepo restic.Repository,
|
||||||
selectedSnapshots iter.Seq[*data.Snapshot], printer progress.Printer) error {
|
selectedSnapshots iter.Seq2[*data.Snapshot, error], printer restic.Printer) error {
|
||||||
|
|
||||||
// remember already processed trees across all snapshots
|
// remember already processed trees across all snapshots
|
||||||
visitedTrees := srcRepo.NewAssociatedBlobSet()
|
visitedTrees := srcRepo.NewAssociatedBlobSet()
|
||||||
@@ -199,7 +215,7 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
|
|||||||
minDuration := 1 * time.Minute
|
minDuration := 1 * time.Minute
|
||||||
|
|
||||||
// use pull-based iterator to allow iteration in multiple steps
|
// use pull-based iterator to allow iteration in multiple steps
|
||||||
next, stop := iter.Pull(selectedSnapshots)
|
next, stop := iter.Pull2(selectedSnapshots)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -210,7 +226,10 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
|
|||||||
// call WithBlobUploader() once and then loop over all selectedSnapshots
|
// call WithBlobUploader() once and then loop over all selectedSnapshots
|
||||||
err := dstRepo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
err := dstRepo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||||
for batchSize < targetSize || time.Since(startTime) < minDuration {
|
for batchSize < targetSize || time.Since(startTime) < minDuration {
|
||||||
sn, ok := next()
|
sn, err, ok := next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -254,8 +273,8 @@ func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo res
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
func copyTree(ctx context.Context, srcRepo *repository.Repository, dstRepo restic.Repository,
|
||||||
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer progress.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
|
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer restic.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
|
||||||
|
|
||||||
copyBlobs := srcRepo.NewAssociatedBlobSet()
|
copyBlobs := srcRepo.NewAssociatedBlobSet()
|
||||||
packList := restic.NewIDSet()
|
packList := restic.NewIDSet()
|
||||||
@@ -264,16 +283,16 @@ func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Rep
|
|||||||
enqueue := func(h restic.BlobHandle) {
|
enqueue := func(h restic.BlobHandle) {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
|
if _, ok := dstRepo.LookupBlobSize(h); !ok {
|
||||||
pb := srcRepo.LookupBlob(h.Type, h.ID)
|
pb := srcRepo.LookupBlob(h)
|
||||||
copyBlobs.Insert(h)
|
copyBlobs.Insert(h)
|
||||||
for _, p := range pb {
|
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}
|
handle := restic.BlobHandle{ID: treeID, Type: restic.TreeBlob}
|
||||||
visited := visitedTrees.Has(handle)
|
visited := visitedTrees.Has(handle)
|
||||||
visitedTrees.Insert(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
|
// 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
|
// count and size
|
||||||
countBlobs := 0
|
countBlobs := 0
|
||||||
sizeBlobs := uint64(0)
|
sizeBlobs := uint64(0)
|
||||||
for blob := range copyBlobs.Keys() {
|
for blob := range copyBlobs.Keys() {
|
||||||
for _, blob := range srcRepo.LookupBlob(blob.Type, blob.ID) {
|
for _, pb := range srcRepo.LookupBlob(blob) {
|
||||||
countBlobs++
|
countBlobs++
|
||||||
sizeBlobs += uint64(blob.Length)
|
sizeBlobs += uint64(pb.CiphertextLength())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,7 +348,7 @@ func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, pa
|
|||||||
return sizeBlobs
|
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.
|
sn.Parent = nil // Parent does not have relevance in the new repo.
|
||||||
// Use Original as a persistent snapshot ID
|
// Use Original as a persistent snapshot ID
|
||||||
if sn.Original == nil {
|
if sn.Original == nil {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunCopy(t testing.TB, srcGopts global.Options, dstGopts global.Options) {
|
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) {
|
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 {
|
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)
|
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
rtest.OK(t, repo.List(context.TODO(), restic.PackFile, func(id restic.ID, size int64) error {
|
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.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:
|
case restic.TreeBlob:
|
||||||
countTreePacks++
|
countTreePacks++
|
||||||
case restic.DataBlob:
|
case restic.DataBlob:
|
||||||
countDataPacks++
|
countDataPacks++
|
||||||
}
|
}
|
||||||
countBlobs += len(blobs)
|
countBlobs += len(handles)
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+12
-366
@@ -4,28 +4,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"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/data"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/repository/index"
|
|
||||||
"github.com/restic/restic/internal/repository/pack"
|
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"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 {
|
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 {
|
if len(args) != 1 {
|
||||||
return errors.Fatal("type not specified")
|
return errors.Fatal("type not specified")
|
||||||
@@ -200,11 +134,11 @@ func runDebugDump(ctx context.Context, gopts global.Options, args []string, term
|
|||||||
|
|
||||||
switch tpe {
|
switch tpe {
|
||||||
case "indexes":
|
case "indexes":
|
||||||
return dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
return repository.DumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||||
case "snapshots":
|
case "snapshots":
|
||||||
return debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
return debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
||||||
case "packs":
|
case "packs":
|
||||||
return printPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
|
return repository.DumpPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||||
case "all":
|
case "all":
|
||||||
printer.S("snapshots:")
|
printer.S("snapshots:")
|
||||||
err := debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
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:")
|
printer.S("indexes:")
|
||||||
err = dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
err = repository.DumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
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 {
|
if opts.ExtractPack && gopts.NoLock {
|
||||||
return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive")
|
return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive")
|
||||||
@@ -478,8 +193,14 @@ func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
examineOpts := repository.ExaminePackOptions{
|
||||||
|
TryRepair: opts.TryRepair,
|
||||||
|
RepairByte: opts.RepairByte,
|
||||||
|
ExtractPack: opts.ExtractPack,
|
||||||
|
ReuploadBlobs: opts.ReuploadBlobs,
|
||||||
|
}
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
err := examinePack(ctx, opts, repo, id, printer)
|
err := repository.ExaminePack(ctx, repo, id, examineOpts, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printer.E("error: %v", err)
|
printer.E("error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -489,78 +210,3 @@ func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamin
|
|||||||
}
|
}
|
||||||
return nil
|
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/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
@@ -83,7 +84,7 @@ type Comparer struct {
|
|||||||
repo restic.BlobLoader
|
repo restic.BlobLoader
|
||||||
opts DiffOptions
|
opts DiffOptions
|
||||||
printChange func(change *Change)
|
printChange func(change *Change)
|
||||||
printError func(string, ...interface{})
|
printError func(string, ...any)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Change struct {
|
type Change struct {
|
||||||
@@ -157,7 +158,7 @@ type DiffStatsContainer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateBlobs updates the blob counters in the stats struct.
|
// updateBlobs updates the blob counters in the stats struct.
|
||||||
func updateBlobs(repo restic.Loader, blobs restic.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() {
|
for h := range blobs.Keys() {
|
||||||
switch h.Type {
|
switch h.Type {
|
||||||
case restic.DataBlob:
|
case restic.DataBlob:
|
||||||
@@ -166,7 +167,7 @@ func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *Diff
|
|||||||
stats.TreeBlobs++
|
stats.TreeBlobs++
|
||||||
}
|
}
|
||||||
|
|
||||||
size, found := repo.LookupBlobSize(h.Type, h.ID)
|
size, found := repo.LookupBlobSize(h)
|
||||||
if !found {
|
if !found {
|
||||||
printError("unable to find blob size for %v", h)
|
printError("unable to find blob size for %v", h)
|
||||||
continue
|
continue
|
||||||
@@ -358,7 +359,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts global.Options, args [
|
|||||||
return errors.Fatalf("specify two snapshot IDs")
|
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)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"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")
|
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 {
|
switch opts.Archive {
|
||||||
case "tar", "zip":
|
case "tar", "zip":
|
||||||
|
|||||||
+54
-51
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,9 +136,9 @@ type statefulOutput struct {
|
|||||||
oldsn *data.Snapshot
|
oldsn *data.Snapshot
|
||||||
hits int
|
hits int
|
||||||
printer interface {
|
printer interface {
|
||||||
S(string, ...interface{})
|
S(string, ...any)
|
||||||
P(string, ...interface{})
|
P(string, ...any)
|
||||||
E(string, ...interface{})
|
E(string, ...any)
|
||||||
}
|
}
|
||||||
stdout io.Writer
|
stdout io.Writer
|
||||||
}
|
}
|
||||||
@@ -213,7 +214,7 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
ParentTree string `json:"parent_tree,omitempty"`
|
ParentTree string `json:"parent_tree,omitempty"`
|
||||||
SnapshotID string `json:"snapshot"`
|
SnapshotID string `json:"snapshot"`
|
||||||
Time time.Time `json:"time,omitempty"`
|
Time time.Time `json:"time"`
|
||||||
}{
|
}{
|
||||||
ObjectType: kind,
|
ObjectType: kind,
|
||||||
ID: id,
|
ID: id,
|
||||||
@@ -280,9 +281,9 @@ type Finder struct {
|
|||||||
treeIDs map[string]struct{}
|
treeIDs map[string]struct{}
|
||||||
itemsFound int
|
itemsFound int
|
||||||
printer interface {
|
printer interface {
|
||||||
S(string, ...interface{})
|
S(string, ...any)
|
||||||
P(string, ...interface{})
|
P(string, ...any)
|
||||||
E(string, ...interface{})
|
E(string, ...any)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +378,7 @@ func (f *Finder) findTree(treeID restic.ID, nodepath string) error {
|
|||||||
f.itemsFound++
|
f.itemsFound++
|
||||||
// Terminate if we have found all trees (and we are not
|
// Terminate if we have found all trees (and we are not
|
||||||
// looking for blobs)
|
// 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 an error to terminate the Walk
|
||||||
return errFindDone
|
return errFindDone
|
||||||
}
|
}
|
||||||
@@ -412,13 +413,13 @@ func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
|
|||||||
return nil
|
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 {
|
if err := f.findTree(*node.Subtree, nodepath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == data.NodeTypeFile && f.blobIDs != nil {
|
if node.Type == data.NodeTypeFile && len(f.blobIDs) > 0 {
|
||||||
for _, id := range node.Content {
|
for _, id := range node.Content {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
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")
|
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
|
// packsToBlobs converts the list of pack IDs to a list of blob IDs that
|
||||||
// belong to those packs.
|
// belong to those packs.
|
||||||
func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
delete(packIDs, id.Str())
|
delete(packIDs, id.Str())
|
||||||
} else {
|
packIDs[idStr] = struct{}{}
|
||||||
// forget found id
|
|
||||||
delete(packIDs, idStr)
|
|
||||||
}
|
}
|
||||||
debug.Log("Found pack %s", idStr)
|
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 {
|
if err != nil {
|
||||||
return err
|
// ignore error to allow fallback to index
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
for _, b := range blobs {
|
for _, h := range handles {
|
||||||
switch b.Type {
|
f.addBlobHandle(h)
|
||||||
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()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// forget successfully processed pack
|
||||||
|
delete(packIDs, idStr)
|
||||||
// Stop searching when all packs have been found
|
// Stop searching when all packs have been found
|
||||||
if len(packIDs) == 0 {
|
if len(packIDs) == 0 {
|
||||||
return errAllPacksFound
|
return errAllPacksFound
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil && !errors.Is(err, errAllPacksFound) {
|
||||||
if err != nil && err != errAllPacksFound {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != errAllPacksFound {
|
if len(packIDs) > 0 {
|
||||||
// try to resolve unknown pack ids from the index
|
// try to resolve unknown pack ids from the index
|
||||||
packIDs, err = f.indexPacksToBlobs(ctx, packIDs)
|
packIDs, err = f.indexPacksToBlobs(ctx, packIDs)
|
||||||
if err != nil {
|
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)
|
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
|
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
|
// remember which packs were found in the index
|
||||||
indexPackIDs := make(map[string]struct{})
|
indexPackIDs := make(map[string]struct{})
|
||||||
err := f.repo.ListBlobs(wctx, func(pb restic.PackedBlob) {
|
err := f.repo.ListBlobs(wctx, func(pb restic.PackBlob) {
|
||||||
idStr := pb.PackID.String()
|
packID := pb.PackID()
|
||||||
|
idStr := packID.String()
|
||||||
// keep entry in packIDs as Each() returns individual index entries
|
// keep entry in packIDs as Each() returns individual index entries
|
||||||
matchingID := false
|
matchingID := false
|
||||||
if _, ok := packIDs[idStr]; ok {
|
if _, ok := packIDs[idStr]; ok {
|
||||||
matchingID = true
|
matchingID = true
|
||||||
} else {
|
} else {
|
||||||
if _, ok := packIDs[pb.PackID.Str()]; ok {
|
if _, ok := packIDs[packID.Str()]; ok {
|
||||||
// expand id
|
// expand id
|
||||||
delete(packIDs, pb.PackID.Str())
|
delete(packIDs, packID.Str())
|
||||||
packIDs[idStr] = struct{}{}
|
packIDs[idStr] = struct{}{}
|
||||||
matchingID = true
|
matchingID = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if matchingID {
|
if matchingID {
|
||||||
f.blobIDs[pb.ID.String()] = struct{}{}
|
f.addBlobHandle(pb.Handle())
|
||||||
indexPackIDs[idStr] = struct{}{}
|
indexPackIDs[idStr] = struct{}{}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -552,13 +558,6 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
|
|||||||
delete(packIDs, id)
|
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
|
return packIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,16 +568,16 @@ func (f *Finder) findObjectPack(id string, t restic.BlobType) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
blobs := f.repo.LookupBlob(t, rid)
|
blobs := f.repo.LookupBlob(restic.BlobHandle{Type: t, ID: rid})
|
||||||
if len(blobs) == 0 {
|
if len(blobs) == 0 {
|
||||||
f.printer.S("Object %s with type %s not found in the index", t.String(), rid.Str())
|
f.printer.S("Object %s with type %s not found in the index", t.String(), rid.Str())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range blobs {
|
for _, b := range blobs {
|
||||||
if b.ID.Equal(rid) {
|
if b.Handle().ID.Equal(rid) {
|
||||||
f.printer.S("Object belongs to pack %s", b.PackID)
|
f.printer.S("Object belongs to pack %s", b.PackID())
|
||||||
f.printer.S(" ... Pack %s: %s", b.PackID.Str(), b.String())
|
f.printer.S(" ... Pack %s: %v", b.PackID().String(), b.Handle())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -599,7 +598,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
|
|||||||
return errors.Fatal("wrong number of arguments")
|
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
|
var err error
|
||||||
pat := findPattern{pattern: args}
|
pat := findPattern{pattern: args}
|
||||||
@@ -676,11 +675,15 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
|
|||||||
}
|
}
|
||||||
|
|
||||||
var filteredSnapshots []*data.Snapshot
|
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)
|
filteredSnapshots = append(filteredSnapshots, sn)
|
||||||
}
|
return nil
|
||||||
if ctx.Err() != nil {
|
})
|
||||||
return ctx.Err()
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(filteredSnapshots, func(i, j int) bool {
|
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 {
|
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) {
|
if err = f.findIDs(ctx, sn); err != nil && !errors.Is(err, errFindDone) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -703,7 +706,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args [
|
|||||||
}
|
}
|
||||||
f.out.Finish()
|
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()
|
f.findObjectsPacks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/backend"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts global.Options, pattern string) []byte {
|
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"`
|
Path string `json:"path,omitempty"`
|
||||||
Permissions string `json:"permissions,omitempty"`
|
Permissions string `json:"permissions,omitempty"`
|
||||||
Size uint64 `json:"size,omitempty"`
|
Size uint64 `json:"size,omitempty"`
|
||||||
Date time.Time `json:"date,omitempty"`
|
Date time.Time `json:"date"`
|
||||||
UID uint32 `json:"uid,omitempty"`
|
UID uint32 `json:"uid,omitempty"`
|
||||||
GID uint32 `json:"gid,omitempty"`
|
GID uint32 `json:"gid,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -153,7 +154,7 @@ type JSONOutput struct {
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
ParentTree string `json:"parent_tree,omitempty"`
|
ParentTree string `json:"parent_tree,omitempty"`
|
||||||
SnapshotID string `json:"snapshot"`
|
SnapshotID string `json:"snapshot"`
|
||||||
Time time.Time `json:"time,omitempty"`
|
Time time.Time `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindPackfile(t *testing.T) {
|
func TestFindPackfile(t *testing.T) {
|
||||||
@@ -169,7 +170,7 @@ func TestFindPackfile(t *testing.T) {
|
|||||||
|
|
||||||
// do all the testing wrapped inside withTermStatus()
|
// do all the testing wrapped inside withTermStatus()
|
||||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
@@ -179,9 +180,10 @@ func TestFindPackfile(t *testing.T) {
|
|||||||
|
|
||||||
packID := restic.ID{}
|
packID := restic.ID{}
|
||||||
done := false
|
done := false
|
||||||
err = repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
err = repo.ListBlobs(ctx, func(pb restic.PackBlob) {
|
||||||
if !done && pb.Type == restic.TreeBlob {
|
h := pb.Handle()
|
||||||
packID = pb.PackID
|
if !done && h.Type == restic.TreeBlob {
|
||||||
|
packID = pb.PackID()
|
||||||
done = true
|
done = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -228,20 +230,20 @@ func TestFindPackID(t *testing.T) {
|
|||||||
dataPackID := restic.ID{}
|
dataPackID := restic.ID{}
|
||||||
treePackID := restic.ID{}
|
treePackID := restic.ID{}
|
||||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
// load Index
|
// 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)
|
// go through all index entries and collect data and tree packfile(s)
|
||||||
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackBlob) {
|
||||||
switch blob.Type {
|
switch blob.Handle().Type {
|
||||||
case restic.DataBlob:
|
case restic.DataBlob:
|
||||||
dataPackID = blob.PackID
|
dataPackID = blob.PackID()
|
||||||
case restic.TreeBlob:
|
case restic.TreeBlob:
|
||||||
treePackID = blob.PackID
|
treePackID = blob.PackID()
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
return nil
|
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",
|
rtest.Assert(t, len(findRes) == numberOfFiles, "expected %d entries for this packfile, got %d",
|
||||||
numberOfFiles, len(findRes))
|
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
|
// look for tree packfile
|
||||||
rtest.Assert(t, !treePackID.IsNull(), "expected to find tree packfile in repo")
|
rtest.Assert(t, !treePackID.IsNull(), "expected to find tree packfile in repo")
|
||||||
packID = treePackID.String()
|
packID = treePackID.String()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"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")
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -199,11 +200,15 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
var snapshots data.Snapshots
|
var snapshots data.Snapshots
|
||||||
removeSnIDs := restic.NewIDSet()
|
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)
|
snapshots = append(snapshots, sn)
|
||||||
}
|
return nil
|
||||||
if ctx.Err() != nil {
|
})
|
||||||
return ctx.Err()
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonGroups []*ForgetGroup
|
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))
|
printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs))
|
||||||
}
|
}
|
||||||
pruneOptions.DryRun = opts.DryRun
|
pruneOptions.DryRun = opts.DryRun
|
||||||
return runPruneWithRepo(ctx, pruneOptions, repo, removeSnIDs, printer)
|
return runPruneWithRepo(ctx, pruneOptions, gopts, repo, removeSnIDs, printer)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"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)")
|
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
|
// use a fixed date for the man pages so that generating them is deterministic
|
||||||
date, err := time.Parse("Jan 2006", "Jan 2017")
|
date, err := time.Parse("Jan 2006", "Jan 2017")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,7 +74,7 @@ func writeManpages(root *cobra.Command, dir string, printer progress.Printer) er
|
|||||||
return doc.GenManTree(root, header, dir)
|
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)
|
printer.PT("writing %s completion file to %v", shell, filename)
|
||||||
var outWriter io.Writer
|
var outWriter io.Writer
|
||||||
if filename != "-" {
|
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")
|
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{})
|
cmdRoot := newRootCommand(&global.Options{})
|
||||||
|
|
||||||
if opts.ManDir != "" {
|
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")
|
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
|
var version uint
|
||||||
switch opts.RepositoryVersion {
|
switch opts.RepositoryVersion {
|
||||||
@@ -108,7 +108,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts global.Options, args [
|
|||||||
return nil
|
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 {
|
if opts.CopyChunkerParameters {
|
||||||
otherGopts, _, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "secondary")
|
otherGopts, _, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "secondary")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ import (
|
|||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunInit(t testing.TB, gopts global.Options) {
|
func testRunInit(t testing.TB, gopts global.Options) {
|
||||||
repository.TestUseLowSecurityKDFParameters(t)
|
repository.TestUseLowSecurityKDFParameters(t)
|
||||||
restic.TestDisableCheckPolynomial(t)
|
restic.TestDisableCheckPolynomial(t)
|
||||||
restic.TestSetLockTimeout(t, 0)
|
repository.TestSetLockTimeout(t, 0)
|
||||||
|
|
||||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return runInit(ctx, InitOptions{}, gopts, nil, gopts.Term)
|
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
|
// create temporary junk files to verify that restic does not trip over them
|
||||||
for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
|
for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
|
||||||
|
rtest.OK(t, os.MkdirAll(filepath.Join(gopts.Repo, path), 0700))
|
||||||
rtest.OK(t, os.WriteFile(filepath.Join(gopts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
|
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
|
var repo *repository.Repository
|
||||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
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
|
return err
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
var otherRepo *repository.Repository
|
var otherRepo *repository.Repository
|
||||||
err = withTermStatus(t, env2.gopts, func(ctx context.Context, gopts global.Options) error {
|
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
|
return err
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"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")
|
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)
|
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -70,7 +71,7 @@ func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, ar
|
|||||||
return addKey(ctx, repo, gopts, opts, printer)
|
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)
|
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -13,7 +14,6 @@ import (
|
|||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunKeyListOtherIDs(t testing.TB, gopts global.Options) []string {
|
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)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
_ = 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)
|
rtest.OK(t, err)
|
||||||
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 2, "")
|
err = repo.SearchKey(ctx, testKeyNewPassword, 2, "")
|
||||||
rtest.OK(t, err)
|
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, "john", key.Username)
|
||||||
rtest.Equals(t, "example.com", key.Hostname)
|
rtest.Equals(t, "example.com", key.Hostname)
|
||||||
return nil
|
return nil
|
||||||
@@ -105,11 +107,13 @@ func testRunKeyPasswdUserHost(t testing.TB, newPassword string, gopts global.Opt
|
|||||||
|
|
||||||
gopts.Password = testKeyNewPassword
|
gopts.Password = testKeyNewPassword
|
||||||
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
_ = 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)
|
rtest.OK(t, err)
|
||||||
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 1, "")
|
err = repo.SearchKey(ctx, testKeyNewPassword, 1, "")
|
||||||
rtest.OK(t, err)
|
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, "john", key.Username)
|
||||||
rtest.Equals(t, "example.com", key.Hostname)
|
rtest.Equals(t, "example.com", key.Hostname)
|
||||||
return nil
|
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")
|
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)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -56,7 +56,7 @@ func runKeyList(ctx context.Context, gopts global.Options, args []string, term u
|
|||||||
return listKeys(ctx, repo, gopts, printer)
|
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 {
|
type keyInfo struct {
|
||||||
Current bool `json:"current"`
|
Current bool `json:"current"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"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")
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -65,7 +66,7 @@ func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptio
|
|||||||
return changePassword(ctx, repo, gopts, opts, printer)
|
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)
|
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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")
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -53,7 +53,7 @@ func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term
|
|||||||
return deleteKey(ctx, repo, args[0], printer)
|
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)
|
id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+59
-19
@@ -2,13 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/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/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -18,10 +21,12 @@ func newListCommand(globalOptions *global.Options) *cobra.Command {
|
|||||||
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
|
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "list [flags] [" + listAllowedArgsUseString + "]",
|
Use: "list [flags] [" + listAllowedArgsUseString + "|packs snapshotID]",
|
||||||
Short: "List objects in the repository",
|
Short: "List objects in the repository",
|
||||||
Long: `
|
Long: `
|
||||||
The "list" command allows listing objects in the repository based on type.
|
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
|
EXIT STATUS
|
||||||
===========
|
===========
|
||||||
@@ -35,19 +40,18 @@ Exit status is 12 if the password is incorrect.
|
|||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runList(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
return runList(cmd.Context(), *globalOptions, args, globalOptions.Term, listAllowedArgsUseString)
|
||||||
},
|
},
|
||||||
ValidArgs: listAllowedArgs,
|
ValidArgs: listAllowedArgs,
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal, listAllowedArgsUseString string) error {
|
||||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
if len(args) != 1 {
|
if len(args) == 0 || (args[0] == "packs" && len(args) > 2) || (args[0] != "packs" && len(args) != 1) {
|
||||||
return errors.Fatal("type not specified")
|
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)
|
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] {
|
switch args[0] {
|
||||||
case "packs":
|
case "packs":
|
||||||
t = restic.PackFile
|
t = restic.PackFile
|
||||||
|
if len(args) == 2 {
|
||||||
|
// args[1] needs to be a snapshotID
|
||||||
|
return packfileList(ctx, repo, args[1], printer)
|
||||||
|
}
|
||||||
case "index":
|
case "index":
|
||||||
t = restic.IndexFile
|
t = restic.IndexFile
|
||||||
case "snapshots":
|
case "snapshots":
|
||||||
@@ -69,18 +77,13 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T
|
|||||||
case "locks":
|
case "locks":
|
||||||
t = restic.LockFile
|
t = restic.LockFile
|
||||||
case "blobs":
|
case "blobs":
|
||||||
return index.ForAllIndexes(ctx, repo, repo, func(_ restic.ID, idx *index.Index, err error) error {
|
for entry := range repository.AllIndexBlobs(ctx, repo, repo) {
|
||||||
if err != nil {
|
if entry.Error != nil {
|
||||||
return err
|
return entry.Error
|
||||||
}
|
}
|
||||||
for blobs := range idx.Values() {
|
printer.S("%v %v", entry.Handle.Type, entry.Handle.ID)
|
||||||
if ctx.Err() != nil {
|
}
|
||||||
return ctx.Err()
|
return nil
|
||||||
}
|
|
||||||
printer.S("%v %v", blobs.Type, blobs.ID)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
default:
|
default:
|
||||||
return errors.Fatal("invalid type")
|
return errors.Fatal("invalid type")
|
||||||
}
|
}
|
||||||
@@ -90,3 +93,40 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T
|
|||||||
return nil
|
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/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
"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 {
|
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)
|
rtest.OK(t, err)
|
||||||
return parseIDsFromReader(t, buf)
|
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
|
// extract blob set from repository index
|
||||||
func testListBlobs(t testing.TB, gopts global.Options) (blobSetFromIndex restic.IDSet) {
|
func testListBlobs(t testing.TB, gopts global.Options) (blobSetFromIndex restic.IDSet) {
|
||||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
// make sure the index is loaded
|
// 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
|
// get blobs from index
|
||||||
blobSetFromIndex = restic.NewIDSet()
|
blobSetFromIndex = restic.NewIDSet()
|
||||||
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackBlob) {
|
||||||
blobSetFromIndex.Insert(blob.ID)
|
blobSetFromIndex.Insert(blob.Handle().ID)
|
||||||
}))
|
}))
|
||||||
return nil
|
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")
|
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/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ func (opts *LsOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
|
|
||||||
type lsPrinter interface {
|
type lsPrinter interface {
|
||||||
Snapshot(sn *data.Snapshot) error
|
Snapshot(sn *data.Snapshot) error
|
||||||
Node(path string, node *data.Node, isPrefixDirectory bool) error
|
NodeOutput(n lsNodeOutput, isPrefixDirectory bool) error
|
||||||
LeaveDir(path string) error
|
LeaveDir(path string) error
|
||||||
Close() 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.
|
type lsNodeOutput struct {
|
||||||
func (p *jsonLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
Name string `json:"name"`
|
||||||
if isPrefixDirectory {
|
Type data.NodeType `json:"type"`
|
||||||
return nil
|
Path string `json:"path"`
|
||||||
}
|
UID uint32 `json:"uid"`
|
||||||
return lsNodeJSON(p.enc, path, node)
|
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 {
|
func (n lsNodeOutput) fileSize() uint64 {
|
||||||
n := &struct {
|
if n.Size == nil {
|
||||||
Name string `json:"name"`
|
return 0
|
||||||
Type string `json:"type"`
|
}
|
||||||
Path string `json:"path"`
|
return *n.Size
|
||||||
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
|
|
||||||
|
|
||||||
size uint64 // Target for Size pointer.
|
func lsNodeOutputFrom(path string, node *data.Node) lsNodeOutput {
|
||||||
}{
|
n := lsNodeOutput{
|
||||||
Name: node.Name,
|
Name: node.Name,
|
||||||
Type: string(node.Type),
|
Type: node.Type,
|
||||||
Path: path,
|
Path: path,
|
||||||
UID: node.UID,
|
UID: node.UID,
|
||||||
GID: node.GID,
|
GID: node.GID,
|
||||||
size: node.Size,
|
|
||||||
Mode: node.Mode,
|
Mode: node.Mode,
|
||||||
Permissions: node.Mode.String(),
|
Permissions: node.Mode.String(),
|
||||||
ModTime: node.ModTime,
|
ModTime: node.ModTime,
|
||||||
AccessTime: node.AccessTime,
|
AccessTime: node.AccessTime,
|
||||||
ChangeTime: node.ChangeTime,
|
ChangeTime: node.ChangeTime,
|
||||||
Inode: node.Inode,
|
Inode: node.Inode,
|
||||||
|
LinkTarget: node.LinkTarget,
|
||||||
|
DeviceID: node.DeviceID,
|
||||||
|
Links: node.Links,
|
||||||
MessageType: "node",
|
MessageType: "node",
|
||||||
StructType: "node",
|
StructType: "node",
|
||||||
}
|
}
|
||||||
// Always print size for regular files, even when empty,
|
// Always print size for regular files, even when empty,
|
||||||
// but never for other types.
|
// but never for other types.
|
||||||
if node.Type == data.NodeTypeFile {
|
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 }
|
func (p *jsonLsPrinter) LeaveDir(_ string) error { return nil }
|
||||||
@@ -195,7 +209,7 @@ func (p *ncduLsPrinter) Snapshot(sn *data.Snapshot) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
|
func lsNcduOutput(node lsNodeOutput) ([]byte, error) {
|
||||||
type NcduNode struct {
|
type NcduNode struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Asize uint64 `json:"asize"`
|
Asize uint64 `json:"asize"`
|
||||||
@@ -212,11 +226,12 @@ func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
|
|||||||
|
|
||||||
const blockSize = 512
|
const blockSize = 512
|
||||||
|
|
||||||
|
size := node.fileSize()
|
||||||
outNode := NcduNode{
|
outNode := NcduNode{
|
||||||
Name: node.Name,
|
Name: node.Name,
|
||||||
Asize: node.Size,
|
Asize: size,
|
||||||
// round up to nearest full blocksize
|
// round up to nearest full blocksize
|
||||||
Dsize: (node.Size + blockSize - 1) / blockSize * blockSize,
|
Dsize: (size + blockSize - 1) / blockSize * blockSize,
|
||||||
Dev: node.DeviceID,
|
Dev: node.DeviceID,
|
||||||
Ino: node.Inode,
|
Ino: node.Inode,
|
||||||
NLink: node.Links,
|
NLink: node.Links,
|
||||||
@@ -244,8 +259,8 @@ func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
|
|||||||
return json.Marshal(outNode)
|
return json.Marshal(outNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ncduLsPrinter) Node(path string, node *data.Node, _ bool) error {
|
func (p *ncduLsPrinter) NodeOutput(node lsNodeOutput, _ bool) error {
|
||||||
out, err := lsNcduNode(path, node)
|
out, err := lsNcduOutput(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -275,8 +290,8 @@ type textLsPrinter struct {
|
|||||||
ListLong bool
|
ListLong bool
|
||||||
HumanReadable bool
|
HumanReadable bool
|
||||||
termPrinter interface {
|
termPrinter interface {
|
||||||
P(msg string, args ...interface{})
|
P(msg string, args ...any)
|
||||||
S(msg string, args ...interface{})
|
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)
|
p.termPrinter.P("%v filtered by %v:", sn, p.dirs)
|
||||||
return nil
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -298,14 +313,8 @@ func (p *textLsPrinter) Close() error {
|
|||||||
return nil
|
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 {
|
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 {
|
if len(args) == 0 {
|
||||||
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
|
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
|
||||||
@@ -431,7 +440,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
|
|||||||
printedDir := false
|
printedDir := false
|
||||||
if withinDir(nodepath) {
|
if withinDir(nodepath) {
|
||||||
// if we're within a target path, print the node
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
printedDir = true
|
printedDir = true
|
||||||
@@ -448,7 +457,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
|
|||||||
if approachingMatchingTree(nodepath) {
|
if approachingMatchingTree(nodepath) {
|
||||||
// print node leading up to the target paths
|
// print node leading up to the target paths
|
||||||
if !printedDir {
|
if !printedDir {
|
||||||
return printer.Node(nodepath, node, true)
|
return printer.NodeOutput(lsNodeOutputFrom(nodepath, node), true)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -487,7 +496,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []str
|
|||||||
|
|
||||||
type sortedPrinter struct {
|
type sortedPrinter struct {
|
||||||
printer lsPrinter
|
printer lsPrinter
|
||||||
collector []toSortOutput
|
collector []lsNodeOutput
|
||||||
sortMode SortMode
|
sortMode SortMode
|
||||||
reverse bool
|
reverse bool
|
||||||
}
|
}
|
||||||
@@ -495,9 +504,9 @@ type sortedPrinter struct {
|
|||||||
func (p *sortedPrinter) Snapshot(sn *data.Snapshot) error {
|
func (p *sortedPrinter) Snapshot(sn *data.Snapshot) error {
|
||||||
return p.printer.Snapshot(sn)
|
return p.printer.Snapshot(sn)
|
||||||
}
|
}
|
||||||
func (p *sortedPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
func (p *sortedPrinter) NodeOutput(node lsNodeOutput, isPrefixDirectory bool) error {
|
||||||
if !isPrefixDirectory {
|
if !isPrefixDirectory {
|
||||||
p.collector = append(p.collector, toSortOutput{path, node})
|
p.collector = append(p.collector, node)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -506,49 +515,49 @@ func (p *sortedPrinter) LeaveDir(_ string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (p *sortedPrinter) Close() error {
|
func (p *sortedPrinter) Close() error {
|
||||||
var comparator func(a, b toSortOutput) int
|
var comparator func(a, b lsNodeOutput) int
|
||||||
switch p.sortMode {
|
switch p.sortMode {
|
||||||
case SortModeName:
|
case SortModeName:
|
||||||
case SortModeSize:
|
case SortModeSize:
|
||||||
comparator = func(a, b toSortOutput) int {
|
comparator = func(a, b lsNodeOutput) int {
|
||||||
return cmp.Or(
|
return cmp.Or(
|
||||||
cmp.Compare(a.node.Size, b.node.Size),
|
cmp.Compare(a.fileSize(), b.fileSize()),
|
||||||
cmp.Compare(a.nodepath, b.nodepath),
|
cmp.Compare(a.Path, b.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case SortModeMtime:
|
case SortModeMtime:
|
||||||
comparator = func(a, b toSortOutput) int {
|
comparator = func(a, b lsNodeOutput) int {
|
||||||
return cmp.Or(
|
return cmp.Or(
|
||||||
a.node.ModTime.Compare(b.node.ModTime),
|
a.ModTime.Compare(b.ModTime),
|
||||||
cmp.Compare(a.nodepath, b.nodepath),
|
cmp.Compare(a.Path, b.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case SortModeAtime:
|
case SortModeAtime:
|
||||||
comparator = func(a, b toSortOutput) int {
|
comparator = func(a, b lsNodeOutput) int {
|
||||||
return cmp.Or(
|
return cmp.Or(
|
||||||
a.node.AccessTime.Compare(b.node.AccessTime),
|
a.AccessTime.Compare(b.AccessTime),
|
||||||
cmp.Compare(a.nodepath, b.nodepath),
|
cmp.Compare(a.Path, b.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case SortModeCtime:
|
case SortModeCtime:
|
||||||
comparator = func(a, b toSortOutput) int {
|
comparator = func(a, b lsNodeOutput) int {
|
||||||
return cmp.Or(
|
return cmp.Or(
|
||||||
a.node.ChangeTime.Compare(b.node.ChangeTime),
|
a.ChangeTime.Compare(b.ChangeTime),
|
||||||
cmp.Compare(a.nodepath, b.nodepath),
|
cmp.Compare(a.Path, b.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case SortModeExt:
|
case SortModeExt:
|
||||||
// map name to extension
|
// map name to extension
|
||||||
mapExt := make(map[string]string, len(p.collector))
|
mapExt := make(map[string]string, len(p.collector))
|
||||||
for _, item := range p.collector {
|
for _, item := range p.collector {
|
||||||
ext := filepath.Ext(item.nodepath)
|
ext := filepath.Ext(item.Path)
|
||||||
mapExt[item.nodepath] = ext
|
mapExt[item.Path] = ext
|
||||||
}
|
}
|
||||||
|
|
||||||
comparator = func(a, b toSortOutput) int {
|
comparator = func(a, b lsNodeOutput) int {
|
||||||
return cmp.Or(
|
return cmp.Or(
|
||||||
cmp.Compare(mapExt[a.nodepath], mapExt[b.nodepath]),
|
cmp.Compare(mapExt[a.Path], mapExt[b.Path]),
|
||||||
cmp.Compare(a.nodepath, b.nodepath),
|
cmp.Compare(a.Path, b.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,7 +569,7 @@ func (p *sortedPrinter) Close() error {
|
|||||||
slices.Reverse(p.collector)
|
slices.Reverse(p.collector)
|
||||||
}
|
}
|
||||||
for _, elem := range 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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -96,12 +96,12 @@ func TestLsNodeJSON(t *testing.T) {
|
|||||||
c := lsTestNodes[i]
|
c := lsTestNodes[i]
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
enc := json.NewEncoder(buf)
|
enc := json.NewEncoder(buf)
|
||||||
err := lsNodeJSON(enc, c.path, &c.Node)
|
err := enc.Encode(lsNodeOutputFrom(c.path, &c.Node))
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
rtest.Equals(t, expect+"\n", buf.String())
|
rtest.Equals(t, expect+"\n", buf.String())
|
||||||
|
|
||||||
// Sanity check: output must be valid JSON.
|
// Sanity check: output must be valid JSON.
|
||||||
var v interface{}
|
var v any
|
||||||
err = json.NewDecoder(buf).Decode(&v)
|
err = json.NewDecoder(buf).Decode(&v)
|
||||||
rtest.OK(t, err)
|
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}`,
|
`{"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]
|
c := lsTestNodes[i]
|
||||||
out, err := lsNcduNode(c.path, &c.Node)
|
out, err := lsNcduOutput(lsNodeOutputFrom(c.path, &c.Node))
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
rtest.Equals(t, expect, string(out))
|
rtest.Equals(t, expect, string(out))
|
||||||
|
|
||||||
// Sanity check: output must be valid JSON.
|
// Sanity check: output must be valid JSON.
|
||||||
var v interface{}
|
var v any
|
||||||
err = json.Unmarshal(out, &v)
|
err = json.Unmarshal(out, &v)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
@@ -138,24 +138,24 @@ func TestLsNcdu(t *testing.T) {
|
|||||||
Hostname: "host",
|
Hostname: "host",
|
||||||
Paths: []string{"/example"},
|
Paths: []string{"/example"},
|
||||||
}))
|
}))
|
||||||
rtest.OK(t, printer.Node("/directory", &data.Node{
|
rtest.OK(t, printer.NodeOutput(lsNodeOutputFrom("/directory", &data.Node{
|
||||||
Type: data.NodeTypeDir,
|
Type: data.NodeTypeDir,
|
||||||
Name: "directory",
|
Name: "directory",
|
||||||
ModTime: modTime,
|
ModTime: modTime,
|
||||||
}, false))
|
}), false))
|
||||||
rtest.OK(t, printer.Node("/directory/data", &data.Node{
|
rtest.OK(t, printer.NodeOutput(lsNodeOutputFrom("/directory/data", &data.Node{
|
||||||
Type: data.NodeTypeFile,
|
Type: data.NodeTypeFile,
|
||||||
Name: "data",
|
Name: "data",
|
||||||
Size: 42,
|
Size: 42,
|
||||||
ModTime: modTime,
|
ModTime: modTime,
|
||||||
}, false))
|
}), false))
|
||||||
rtest.OK(t, printer.LeaveDir("/directory"))
|
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,
|
Type: data.NodeTypeFile,
|
||||||
Name: "file",
|
Name: "file",
|
||||||
Size: 12345,
|
Size: 12345,
|
||||||
ModTime: modTime,
|
ModTime: modTime,
|
||||||
}, false))
|
}), false))
|
||||||
rtest.OK(t, printer.Close())
|
rtest.OK(t, printer.Close())
|
||||||
|
|
||||||
rtest.Equals(t, `[1, 2, {"time":"0001-01-01T00:00:00Z","tree":null,"paths":["/example"],"hostname":"host"}, [{"name":"/"},
|
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`)
|
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")
|
printer.P("available migrations:\n")
|
||||||
found := false
|
found := false
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ func checkMigrations(ctx context.Context, repo restic.Repository, printer progre
|
|||||||
return nil
|
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
|
var firsterr error
|
||||||
for _, name := range args {
|
for _, name := range args {
|
||||||
found := false
|
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 {
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+8
-20
@@ -19,13 +19,15 @@ import (
|
|||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/fs"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/fuse"
|
"github.com/restic/restic/internal/fuse"
|
||||||
|
|
||||||
systemFuse "github.com/anacrolix/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) {
|
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 {
|
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 == "" {
|
if opts.TimeTemplate == "" {
|
||||||
return errors.Fatal("time template string cannot be empty")
|
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)
|
debug.Log("fuse: %v", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +198,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
err = fs.Serve(c, root)
|
err = fusefs.Serve(c, root)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
printer.S("Now serving the repository at %s", mountpoint)
|
printer.S("Now serving the repository at %s", mountpoint)
|
||||||
@@ -270,9 +272,9 @@ func checkMountpointOverlap(repoPath, mountpoint string) error {
|
|||||||
switch {
|
switch {
|
||||||
case rp == mp:
|
case rp == mp:
|
||||||
return errors.Fatal(fmt.Sprintf("mountpoint %s is the local repository directory%s", mp, tail))
|
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))
|
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 errors.Fatal(fmt.Sprintf("local repository directory %s is inside the mountpoint %s%s", rp, mp, tail))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -293,17 +295,3 @@ func resolvePath(p string) (string, error) {
|
|||||||
}
|
}
|
||||||
return resolved, nil
|
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"
|
systemFuse "github.com/anacrolix/fuse"
|
||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/debug"
|
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mountWait = 20
|
mountWait = 400
|
||||||
mountSleep = 100 * time.Millisecond
|
mountSleep = 5 * time.Millisecond
|
||||||
mountTestSubdir = "snapshots"
|
mountTestSubdir = "snapshots"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ func snapshotsDirExists(t testing.TB, dir string) bool {
|
|||||||
// waitForMount blocks (max mountWait * mountSleep) until the subdir
|
// waitForMount blocks (max mountWait * mountSleep) until the subdir
|
||||||
// "snapshots" appears in the dir.
|
// "snapshots" appears in the dir.
|
||||||
func waitForMount(t testing.TB, dir string) {
|
func waitForMount(t testing.TB, dir string) {
|
||||||
for i := 0; i < mountWait; i++ {
|
for range mountWait {
|
||||||
if snapshotsDirExists(t, dir) {
|
if snapshotsDirExists(t, dir) {
|
||||||
t.Log("mounted directory is ready")
|
t.Log("mounted directory is ready")
|
||||||
return
|
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) {
|
func testRunUmount(t testing.TB, dir string) {
|
||||||
var err error
|
var err error
|
||||||
for i := 0; i < mountWait; i++ {
|
for range mountWait {
|
||||||
if err = systemFuse.Unmount(dir); err == nil {
|
if err = systemFuse.Unmount(dir); err == nil {
|
||||||
t.Logf("directory %v umounted", dir)
|
t.Logf("directory %v umounted", dir)
|
||||||
return
|
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 {
|
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)
|
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -274,11 +273,6 @@ func TestMountSameTimestamps(t *testing.T) {
|
|||||||
t.Skip("Skipping fuse tests")
|
t.Skip("Skipping fuse tests")
|
||||||
}
|
}
|
||||||
|
|
||||||
debugEnabled := debug.TestLogToStderr(t)
|
|
||||||
if debugEnabled {
|
|
||||||
defer debug.TestDisableLog(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
// must list snapshots more than once
|
// must list snapshots more than once
|
||||||
env.gopts.BackendTestHook = nil
|
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")
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -190,11 +190,11 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts global.Options, term
|
|||||||
opts.unsafeRecovery = true
|
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 {
|
func runPruneWithRepo(ctx context.Context, opts PruneOptions, gopts global.Options, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer restic.Printer) error {
|
||||||
if repo.Cache() == nil {
|
if repo.Cache() == nil && !gopts.JSON {
|
||||||
printer.S("warning: running prune without a cache, this may be very slow!")
|
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:")
|
printer.P("\nWould have made the following changes:")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = printPruneStats(printer, plan.Stats())
|
if !gopts.JSON {
|
||||||
if err != nil {
|
err = printPruneStats(printer, plan.Stats())
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gopts.Term.Print(ui.ToJSONString(plan.Stats()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger GC to reset garbage collection threshold
|
// 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
|
// 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))
|
printer.V("\nused: %10d blobs / %s", stats.Blobs.Used, ui.FormatBytes(stats.Size.Used))
|
||||||
if stats.Blobs.Duplicate > 0 {
|
if stats.Blobs.Duplicate > 0 {
|
||||||
printer.V("duplicates: %10d blobs / %s", stats.Blobs.Duplicate, ui.FormatBytes(stats.Size.Duplicate))
|
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 {
|
if stats.Size.Unref > 0 {
|
||||||
printer.V("unreferenced: %s", ui.FormatBytes(stats.Size.Unref))
|
printer.V("unreferenced: %s", ui.FormatBytes(stats.Size.Unref))
|
||||||
}
|
}
|
||||||
totalBlobs := stats.Blobs.Used + stats.Blobs.Unused + stats.Blobs.Duplicate
|
printer.V("total: %10d blobs / %s", stats.Blobs.Total, ui.FormatBytes(stats.Size.Total))
|
||||||
totalSize := stats.Size.Used + stats.Size.Duplicate + stats.Size.Unused + stats.Size.Unref
|
printer.V("unused size: %s of total size", ui.FormatPercent(stats.Size.Duplicate+stats.Size.Unused, stats.Size.Total))
|
||||||
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.P("\nto repack: %10d blobs / %s", stats.Blobs.Repack, ui.FormatBytes(stats.Size.Repack))
|
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("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))
|
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.RemoveTotal, ui.FormatBytes(stats.Size.RemoveTotal))
|
||||||
printer.P("total prune: %10d blobs / %s", stats.Blobs.Remove+stats.Blobs.Repackrm, ui.FormatBytes(totalPruneSize))
|
|
||||||
if stats.Size.Uncompressed > 0 {
|
if stats.Size.Uncompressed > 0 {
|
||||||
printer.P("not yet compressed: %s", ui.FormatBytes(stats.Size.Uncompressed))
|
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))
|
printer.P("remaining: %10d blobs / %s", stats.Blobs.Remain, ui.FormatBytes(stats.Size.Remain))
|
||||||
unusedAfter := unusedSize - stats.Size.Remove - stats.Size.Repackrm
|
|
||||||
printer.P("unused size after prune: %s (%s of remaining size)",
|
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.P("")
|
||||||
printer.V("totally used packs: %10d", stats.Packs.Used)
|
printer.V("totally used packs: %10d", stats.Packs.Used)
|
||||||
printer.V("partly used packs: %10d", stats.Packs.PartlyUsed)
|
printer.V("partly used packs: %10d", stats.Packs.PartlyUsed)
|
||||||
@@ -283,7 +282,7 @@ func printPruneStats(printer progress.Printer, stats repository.PruneStats) erro
|
|||||||
return nil
|
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
|
var snapshotTrees restic.IDs
|
||||||
printer.P("loading all snapshots...")
|
printer.P("loading all snapshots...")
|
||||||
err := data.ForAllSnapshots(ctx, repo, repo, ignoreSnapshots,
|
err := data.ForAllSnapshots(ctx, repo, repo, ignoreSnapshots,
|
||||||
|
|||||||
@@ -258,3 +258,28 @@ func TestPruneRepackSmallerThanSmoke(t *testing.T) {
|
|||||||
MaxUnused: "5%",
|
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
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// tree. If it is not referenced, we have a root tree.
|
||||||
trees := make(map[restic.ID]bool)
|
trees := make(map[restic.ID]bool)
|
||||||
|
|
||||||
err = repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
err = repo.ListBlobs(ctx, func(blob restic.PackBlob) {
|
||||||
if blob.Type == restic.TreeBlob {
|
h := blob.Handle()
|
||||||
trees[blob.Blob.ID] = false
|
if h.Type == restic.TreeBlob {
|
||||||
|
trees[h.ID] = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
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())
|
sn, err := data.NewSnapshot([]string{name}, tags, hostname, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
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/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"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 {
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/backend"
|
"github.com/restic/restic/internal/backend"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/global"
|
"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/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
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) {
|
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)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
|
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
|
||||||
rtest.SetupTarTestFixture(t, env.base, datafile)
|
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") {
|
if !strings.Contains(out, "contained in several indexes") {
|
||||||
t.Fatalf("did not find checker hint for packs 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)
|
testRunRebuildIndex(t, env.gopts)
|
||||||
|
|
||||||
env.gopts.BackendTestHook = nil
|
env.gopts.BackendTestHook = nil
|
||||||
out, err = testRunCheckOutput(t, env.gopts, false)
|
out, _, err = testRunCheckOutput(t, env.gopts, false)
|
||||||
if len(out) != 0 {
|
if len(out) != 0 {
|
||||||
t.Fatalf("expected no output from the checker, got: %v", out)
|
t.Fatalf("expected no output from the checker, got: %v", out)
|
||||||
}
|
}
|
||||||
@@ -61,15 +62,6 @@ func TestRebuildIndex(t *testing.T) {
|
|||||||
testRebuildIndex(t, nil)
|
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.
|
// indexErrorBackend modifies the first index after reading.
|
||||||
type indexErrorBackend struct {
|
type indexErrorBackend struct {
|
||||||
backend.Backend
|
backend.Backend
|
||||||
@@ -82,7 +74,7 @@ func (b *indexErrorBackend) Load(ctx context.Context, h backend.Handle, length i
|
|||||||
// protect hasErred
|
// protect hasErred
|
||||||
b.lock.Lock()
|
b.lock.Lock()
|
||||||
defer b.lock.Unlock()
|
defer b.lock.Unlock()
|
||||||
if !b.hasErred && h.Type == restic.IndexFile {
|
if !b.hasErred && h.Type == backend.IndexFile {
|
||||||
b.hasErred = true
|
b.hasErred = true
|
||||||
return consumer(errorReadCloser{rd})
|
return consumer(errorReadCloser{rd})
|
||||||
}
|
}
|
||||||
@@ -120,6 +112,7 @@ func (b *appendOnlyBackend) Remove(_ context.Context, h backend.Handle) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
|
func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
|
||||||
|
repository.TestInjectKey(t, restic.TestParseID("b9883c60bed42db51be171ca52f055104b6ea7cfa2bc381c05b2b1f78231280c"), `{"mac":{"k":"maQ4ILA872XnDxHVEno94A==","r":"OptMBABwkgIsMQcHME8cBw=="},"encrypt":"janrR1efN7HyQ8kOZ9zhHixooZ/e+WelH0mT4v9WskQ="}`)
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"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")
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
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")
|
printer.P("saving backup copies of pack files to current folder")
|
||||||
for id := range ids {
|
for id := range ids {
|
||||||
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
|
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
|
||||||
// corrupted data is fine
|
if ctx.Err() != nil {
|
||||||
if buf == nil {
|
return ctx.Err()
|
||||||
return 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)
|
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/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -77,8 +78,39 @@ func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
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 {
|
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)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,10 +127,11 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Three error cases are checked:
|
// Four error cases are checked:
|
||||||
// - tree is a nil tree (-> will be replaced by an empty tree)
|
// - tree is a nil tree (-> will be replaced by an empty tree)
|
||||||
// - trees which cannot be loaded (-> the tree contents will be removed)
|
// - trees which cannot be loaded (-> the tree contents will be removed)
|
||||||
// - files whose contents are not fully available (-> file will be modified)
|
// - files whose contents are not fully available (-> file will be modified)
|
||||||
|
// - *checker.SnapshotError
|
||||||
rewriter := walker.NewTreeRewriter(walker.RewriteOpts{
|
rewriter := walker.NewTreeRewriter(walker.RewriteOpts{
|
||||||
RewriteNode: func(node *data.Node, path string) *data.Node {
|
RewriteNode: func(node *data.Node, path string) *data.Node {
|
||||||
if node.Type == data.NodeTypeIrregular || node.Type == data.NodeTypeInvalid {
|
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
|
var newSize uint64
|
||||||
// check all contents and remove if not available
|
// check all contents and remove if not available
|
||||||
for _, id := range node.Content {
|
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
|
ok = false
|
||||||
} else {
|
} else {
|
||||||
newContent = append(newContent, id)
|
newContent = append(newContent, id)
|
||||||
@@ -145,7 +178,15 @@ func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOp
|
|||||||
})
|
})
|
||||||
|
|
||||||
changedCount := 0
|
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)
|
printer.P("\n%v", sn)
|
||||||
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
|
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
|
||||||
func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error) {
|
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 {
|
if changed {
|
||||||
changedCount++
|
changedCount++
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
if ctx.Err() != nil {
|
})
|
||||||
return ctx.Err()
|
|
||||||
|
if errOuter != nil {
|
||||||
|
return errOuter
|
||||||
}
|
}
|
||||||
|
|
||||||
printer.P("")
|
printer.P("")
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -67,7 +69,7 @@ func TestRepairSnapshotsWithLostData(t *testing.T) {
|
|||||||
// repository must be ok after removing the broken snapshots
|
// repository must be ok after removing the broken snapshots
|
||||||
testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
|
testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
|
||||||
testListSnapshots(t, env.gopts, 2)
|
testListSnapshots(t, env.gopts, 2)
|
||||||
_, err := testRunCheckOutput(t, env.gopts, false)
|
_, _, err := testRunCheckOutput(t, env.gopts, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +98,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) {
|
|||||||
testRunRebuildIndex(t, env.gopts)
|
testRunRebuildIndex(t, env.gopts)
|
||||||
testRunRepairSnapshot(t, env.gopts, true)
|
testRunRepairSnapshot(t, env.gopts, true)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
_, err := testRunCheckOutput(t, env.gopts, false)
|
_, _, err := testRunCheckOutput(t, env.gopts, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +121,7 @@ func TestRepairSnapshotsWithLostRootTree(t *testing.T) {
|
|||||||
testRunRebuildIndex(t, env.gopts)
|
testRunRebuildIndex(t, env.gopts)
|
||||||
testRunRepairSnapshot(t, env.gopts, true)
|
testRunRepairSnapshot(t, env.gopts, true)
|
||||||
testListSnapshots(t, env.gopts, 0)
|
testListSnapshots(t, env.gopts, 0)
|
||||||
_, err := testRunCheckOutput(t, env.gopts, false)
|
_, _, err := testRunCheckOutput(t, env.gopts, false)
|
||||||
rtest.OK(t, err)
|
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)
|
rtest.Assert(t, reflect.DeepEqual(oldSnapshotIDs, snapshotIDs), "unexpected snapshot id mismatch %v vs. %v", oldSnapshotIDs, snapshotIDs)
|
||||||
testRunCheck(t, env.gopts)
|
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/errors"
|
||||||
"github.com/restic/restic/internal/filter"
|
"github.com/restic/restic/internal/filter"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/restorer"
|
"github.com/restic/restic/internal/restorer"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
|
||||||
restoreui "github.com/restic/restic/internal/ui/restore"
|
restoreui "github.com/restic/restic/internal/ui/restore"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -167,7 +167,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options,
|
|||||||
return err
|
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{
|
res := restorer.NewRestorer(repo, sn, restorer.Options{
|
||||||
DryRun: opts.DryRun,
|
DryRun: opts.DryRun,
|
||||||
Sparse: opts.Sparse,
|
Sparse: opts.Sparse,
|
||||||
@@ -281,7 +281,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options,
|
|||||||
return nil
|
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
|
hasXattrExcludes := len(opts.ExcludeXattrPattern) > 0
|
||||||
hasXattrIncludes := len(opts.IncludeXattrPattern) > 0
|
hasXattrIncludes := len(opts.IncludeXattrPattern) > 0
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
@@ -242,7 +243,7 @@ func TestRestore(t *testing.T) {
|
|||||||
|
|
||||||
testRunInit(t, env.gopts)
|
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))
|
p := filepath.Join(env.testdata, fmt.Sprintf("foo/bar/testfile%v", i))
|
||||||
rtest.OK(t, os.MkdirAll(filepath.Dir(p), 0755))
|
rtest.OK(t, os.MkdirAll(filepath.Dir(p), 0755))
|
||||||
rtest.OK(t, appendRandomData(p, uint(rand.Intn(2<<21))))
|
rtest.OK(t, appendRandomData(p, uint(rand.Intn(2<<21))))
|
||||||
@@ -329,6 +330,7 @@ func TestRestoreLatest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRestoreWithPermissionFailure(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)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
// be updated accordingly.
|
// be updated accordingly.
|
||||||
type rewriteFilterFunc func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error)
|
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 {
|
if sn.Tree == nil {
|
||||||
return false, errors.Errorf("snapshot %v has nil tree", sn.ID().Str())
|
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,
|
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) {
|
keepEmptySnapshot bool) (bool, error) {
|
||||||
|
|
||||||
var filteredTree restic.ID
|
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")
|
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 (
|
var (
|
||||||
repo *repository.Repository
|
repo *repository.Repository
|
||||||
@@ -327,7 +327,10 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
|
|||||||
}
|
}
|
||||||
|
|
||||||
changedCount := 0
|
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)
|
printer.P("\n%v", sn)
|
||||||
changed, err := rewriteSnapshot(ctx, repo, sn, opts, printer)
|
changed, err := rewriteSnapshot(ctx, repo, sn, opts, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -336,9 +339,10 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
|
|||||||
if changed {
|
if changed {
|
||||||
changedCount++
|
changedCount++
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
if ctx.Err() != nil {
|
})
|
||||||
return ctx.Err()
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
printer.P("")
|
printer.P("")
|
||||||
@@ -359,7 +363,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options,
|
|||||||
return nil
|
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 {
|
inSelectByName := func(nodepath string, node *data.Node) bool {
|
||||||
for _, include := range includeByNameFuncs {
|
for _, include := range includeByNameFuncs {
|
||||||
matched, childMayMatch := include(nodepath)
|
matched, childMayMatch := include(nodepath)
|
||||||
@@ -406,7 +410,7 @@ func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer
|
|||||||
return rewriteNode, keepEmptyDirectory
|
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 {
|
exSelectByName := func(nodepath string) bool {
|
||||||
for _, reject := range excludeByNameFuncs {
|
for _, reject := range excludeByNameFuncs {
|
||||||
if reject(nodepath) {
|
if reject(nodepath) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
"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) {
|
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()
|
t.Helper()
|
||||||
out := testRunLsWithOpts(t, gopts, lsOpts, lsArgs)
|
out := testRunLsWithOpts(t, gopts, lsOpts, lsArgs)
|
||||||
count := 0
|
count := 0
|
||||||
for _, line := range strings.Split(string(out), "\n") {
|
for line := range strings.SplitSeq(string(out), "\n") {
|
||||||
if strings.Contains(line, substring) {
|
if strings.Contains(line, substring) {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@ func getSnapshot(t testing.TB, snapshotID restic.ID, env *testEnvironment) *data
|
|||||||
|
|
||||||
var snapshots []*data.Snapshot
|
var snapshots []*data.Snapshot
|
||||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
@@ -157,7 +158,7 @@ func testRewriteMetadata(t *testing.T, metadata snapshotMetadataArgs) {
|
|||||||
|
|
||||||
var snapshots []*data.Snapshot
|
var snapshots []*data.Snapshot
|
||||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
@@ -205,7 +206,7 @@ func TestRewriteSnaphotSummary(t *testing.T) {
|
|||||||
// replace snapshot by one without a summary
|
// replace snapshot by one without a summary
|
||||||
var oldSummary *data.SnapshotSummary
|
var oldSummary *data.SnapshotSummary
|
||||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
_, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/selfupdate"
|
"github.com/restic/restic/internal/selfupdate"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
@@ -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)
|
printer.P("writing restic to %v", opts.Output)
|
||||||
|
|
||||||
v, err := selfupdate.DownloadLatestStableRelease(ctx, opts.Output, global.Version, printer.P)
|
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/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
"github.com/restic/restic/internal/ui/table"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"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 {
|
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)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -89,12 +90,17 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Option
|
|||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
var snapshots data.Snapshots
|
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)
|
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)
|
snapshotGroups, grouped, err := data.GroupSnapshots(snapshots, opts.GroupBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+33
-107
@@ -7,18 +7,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/restic/chunker"
|
"github.com/restic/chunker"
|
||||||
"github.com/restic/restic/internal/crypto"
|
|
||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/restorer"
|
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
|
statsui "github.com/restic/restic/internal/ui/stats"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
"github.com/restic/restic/internal/ui/table"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
|
|
||||||
@@ -104,7 +101,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
|
|||||||
return err
|
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)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -137,17 +134,19 @@ func runStats(ctx context.Context, opts StatsOptions, gopts global.Options, args
|
|||||||
}
|
}
|
||||||
|
|
||||||
var snapshots data.Snapshots
|
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)
|
snapshots = append(snapshots, sn)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
statsProgress := newStatsProgress(term, !gopts.JSON, uint64(len(snapshots)))
|
statsProgress := statsui.NewProgress(term, gopts.Quiet, gopts.JSON, uint64(len(snapshots)))
|
||||||
|
defer statsProgress.Done()
|
||||||
updater := progress.NewUpdater(ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()), func(runtime time.Duration, final bool) {
|
|
||||||
statsProgress.printProgress(runtime, final)
|
|
||||||
})
|
|
||||||
|
|
||||||
defer updater.Done()
|
|
||||||
|
|
||||||
for _, sn := range snapshots {
|
for _, sn := range snapshots {
|
||||||
err = statsWalkSnapshot(ctx, sn, repo, opts, stats, statsProgress)
|
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 {
|
if opts.countMode == countModeRawData {
|
||||||
// the blob handles have been collected, but not yet counted
|
// the blob handles have been collected, but not yet counted
|
||||||
for blobHandle := range stats.blobs.Keys() {
|
for blobHandle := range stats.blobs.Keys() {
|
||||||
pbs := repo.LookupBlob(blobHandle.Type, blobHandle.ID)
|
pbs := repo.LookupBlob(blobHandle)
|
||||||
if len(pbs) == 0 {
|
if len(pbs) == 0 {
|
||||||
return fmt.Errorf("blob %v not found", blobHandle)
|
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 {
|
if repo.Config().Version >= 2 {
|
||||||
stats.TotalUncompressedSize += uint64(crypto.CiphertextLength(int(pbs[0].DataLength())))
|
stats.TotalUncompressedSize += uint64(pbs[0].UncompressedCiphertextLength())
|
||||||
if pbs[0].IsCompressed() {
|
if pbs[0].IsCompressed() {
|
||||||
stats.TotalCompressedBlobsSize += uint64(pbs[0].Length)
|
stats.TotalCompressedBlobsSize += uint64(pbs[0].CiphertextLength())
|
||||||
stats.TotalCompressedBlobsUncompressedSize += uint64(crypto.CiphertextLength(int(pbs[0].DataLength())))
|
stats.TotalCompressedBlobsUncompressedSize += uint64(pbs[0].UncompressedCiphertextLength())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stats.TotalBlobCount++
|
stats.TotalBlobCount++
|
||||||
statsProgress.update(0, 1, uint64(pbs[0].Length))
|
statsProgress.Update(0, 1, uint64(pbs[0].CiphertextLength()))
|
||||||
}
|
}
|
||||||
if stats.TotalCompressedBlobsSize > 0 {
|
if stats.TotalCompressedBlobsSize > 0 {
|
||||||
stats.CompressionRatio = float64(stats.TotalCompressedBlobsUncompressedSize) / float64(stats.TotalCompressedBlobsSize)
|
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
|
// stop progress bar to prevent mangled output
|
||||||
updater.Done()
|
statsProgress.Done()
|
||||||
|
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
err = json.NewEncoder(gopts.Term.OutputWriter()).Encode(stats)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic.Loader, opts StatsOptions, stats *statsContainer, progress *statsProgress) error {
|
func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic.Loader, opts StatsOptions, stats *statsContainer, sp *statsui.Progress) error {
|
||||||
progress.processSnapshot()
|
sp.ProcessSnapshot()
|
||||||
if snapshot.Tree == nil {
|
if snapshot.Tree == nil {
|
||||||
return fmt.Errorf("snapshot %s has nil tree", snapshot.ID().Str())
|
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 {
|
if opts.countMode == countModeRawData {
|
||||||
// count just the sizes of unique blobs; we don't need to walk the tree
|
// 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
|
// 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{
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("walking tree %s: %v", *snapshot.Tree, err)
|
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
|
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 {
|
return func(parentTreeID restic.ID, npath string, node *data.Node, nodeErr error) error {
|
||||||
if nodeErr != nil {
|
if nodeErr != nil {
|
||||||
return nodeErr
|
return nodeErr
|
||||||
@@ -254,7 +253,7 @@ func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer,
|
|||||||
if node == nil {
|
if node == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
progress.update(1, 0, uint64(node.Size))
|
progress.Update(1, 0, uint64(node.Size))
|
||||||
if opts.countMode == countModeUniqueFilesByContents || opts.countMode == countModeBlobsPerFile {
|
if opts.countMode == countModeUniqueFilesByContents || opts.countMode == countModeBlobsPerFile {
|
||||||
// only count this file if we haven't visited it before
|
// only count this file if we haven't visited it before
|
||||||
fid := makeFileIDByContents(node)
|
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
|
// ensure we have this file (by path) in our map; in this
|
||||||
// mode, a file is unique by both contents and path
|
// mode, a file is unique by both contents and path
|
||||||
nodePath := filepath.Join(npath, node.Name)
|
nodePath := filepath.Join(npath, node.Name)
|
||||||
progress.update(0, 1, 0)
|
progress.Update(0, 1, 0)
|
||||||
if _, ok := stats.fileBlobs[nodePath]; !ok {
|
if _, ok := stats.fileBlobs[nodePath]; !ok {
|
||||||
stats.fileBlobs[nodePath] = restic.NewIDSet()
|
stats.fileBlobs[nodePath] = restic.NewIDSet()
|
||||||
stats.TotalFileCount++
|
stats.TotalFileCount++
|
||||||
}
|
}
|
||||||
if _, ok := stats.fileBlobs[nodePath][blobID]; !ok {
|
if _, ok := stats.fileBlobs[nodePath][blobID]; !ok {
|
||||||
// is always a data blob since we're accessing it via a file's Content array
|
// 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 {
|
if !found {
|
||||||
return fmt.Errorf("blob %s not found for tree %s", blobID, parentTreeID)
|
return fmt.Errorf("blob %s not found for tree %s", blobID, parentTreeID)
|
||||||
}
|
}
|
||||||
@@ -371,76 +370,6 @@ type statsContainer struct {
|
|||||||
// independent of references to files
|
// independent of references to files
|
||||||
blobs restic.AssociatedBlobSet
|
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.
|
// fileID is a 256-bit hash that distinguishes unique files.
|
||||||
type fileID [32]byte
|
type fileID [32]byte
|
||||||
@@ -453,7 +382,7 @@ const (
|
|||||||
countModeDebug = "debug"
|
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")
|
printer.E("Collecting size statistics\n\n")
|
||||||
for _, t := range []restic.FileType{restic.KeyFile, restic.LockFile, restic.IndexFile, restic.SnapshotFile, restic.PackFile} {
|
for _, t := range []restic.FileType{restic.KeyFile, restic.LockFile, restic.IndexFile, restic.SnapshotFile, restic.PackFile} {
|
||||||
hist, err := statsDebugFileType(ctx, repo, t)
|
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) {
|
func statsDebugBlobs(ctx context.Context, repo restic.Repository) ([restic.NumBlobTypes]*sizeHistogram, error) {
|
||||||
var hist [restic.NumBlobTypes]*sizeHistogram
|
var hist [restic.NumBlobTypes]*sizeHistogram
|
||||||
for i := 0; i < len(hist); i++ {
|
for i := range len(hist) {
|
||||||
hist[i] = newSizeHistogram(2 * chunker.MaxSize)
|
hist[i] = newSizeHistogram(2 * chunker.MaxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
err := repo.ListBlobs(ctx, func(pb restic.PackBlob) {
|
||||||
hist[pb.Type].Add(uint64(pb.Length))
|
hist[pb.Handle().Type].Add(uint64(pb.CiphertextLength()))
|
||||||
})
|
})
|
||||||
|
|
||||||
return hist, err
|
return hist, err
|
||||||
@@ -517,10 +446,7 @@ func newSizeHistogram(sizeLimit uint64) *sizeHistogram {
|
|||||||
growthFactor := uint64(10)
|
growthFactor := uint64(10)
|
||||||
|
|
||||||
for lowerBound < sizeLimit {
|
for lowerBound < sizeLimit {
|
||||||
upperBound := lowerBound*growthFactor - 1
|
upperBound := min(lowerBound*growthFactor-1, sizeLimit)
|
||||||
if upperBound > sizeLimit {
|
|
||||||
upperBound = sizeLimit
|
|
||||||
}
|
|
||||||
h.buckets = append(h.buckets, sizeClass{lowerBound, upperBound, 0})
|
h.buckets = append(h.buckets, sizeClass{lowerBound, upperBound, 0})
|
||||||
lowerBound *= growthFactor
|
lowerBound *= growthFactor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSizeHistogramNew(t *testing.T) {
|
func TestSizeHistogramNew(t *testing.T) {
|
||||||
@@ -26,7 +24,7 @@ func TestSizeHistogramNew(t *testing.T) {
|
|||||||
|
|
||||||
func TestSizeHistogramAdd(t *testing.T) {
|
func TestSizeHistogramAdd(t *testing.T) {
|
||||||
h := newSizeHistogram(42)
|
h := newSizeHistogram(42)
|
||||||
for i := uint64(0); i < 45; i++ {
|
for i := range uint64(45) {
|
||||||
h.Add(i)
|
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())
|
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/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTagCommand(globalOptions *global.Options) *cobra.Command {
|
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 {
|
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 {
|
if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 {
|
||||||
return errors.Fatal("nothing to do!")
|
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)
|
changed, err := changeTags(ctx, repo, sn, opts.SetTags.Flatten(), opts.AddTags.Flatten(), opts.RemoveTags.Flatten(), printFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printer.E("unable to modify the tags for snapshot ID %q, ignoring: %v", sn.ID(), err)
|
printer.E("unable to modify the tags for snapshot ID %q, ignoring: %v", sn.ID(), err)
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
summary.ChangedSnapshots++
|
summary.ChangedSnapshots++
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
|
})
|
||||||
if ctx.Err() != nil {
|
if err != nil {
|
||||||
return ctx.Err()
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
printSummary(summary)
|
printSummary(summary)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"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 {
|
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)
|
repo, err := global.OpenRepository(ctx, gopts, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ Exit status is 1 if there was any error.
|
|||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
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 {
|
if globalOptions.JSON {
|
||||||
type jsonVersion struct {
|
type jsonVersion struct {
|
||||||
|
|||||||
+1
-34
@@ -1,17 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/restic"
|
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// initMultiSnapshotFilter is used for commands that work on multiple snapshots
|
// 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
|
// MUST be followed by finalizeSnapshotFilter after flag parsing
|
||||||
func initMultiSnapshotFilter(flags *pflag.FlagSet, filt *data.SnapshotFilter, addHostShorthand bool) {
|
func initMultiSnapshotFilter(flags *pflag.FlagSet, filt *data.SnapshotFilter, addHostShorthand bool) {
|
||||||
hostShorthand := "H"
|
hostShorthand := "H"
|
||||||
@@ -47,33 +44,3 @@ func finalizeSnapshotFilter(filt *data.SnapshotFilter) {
|
|||||||
filt.Hosts = nil
|
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"
|
"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 {
|
if !long {
|
||||||
return path
|
return n.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
var mode os.FileMode
|
var mode os.FileMode
|
||||||
@@ -19,9 +19,9 @@ func formatNode(path string, n *data.Node, long bool, human bool) string {
|
|||||||
|
|
||||||
var size string
|
var size string
|
||||||
if human {
|
if human {
|
||||||
size = ui.FormatBytes(n.Size)
|
size = ui.FormatBytes(n.fileSize())
|
||||||
} else {
|
} else {
|
||||||
size = fmt.Sprintf("%6d", n.Size)
|
size = fmt.Sprintf("%6d", n.fileSize())
|
||||||
}
|
}
|
||||||
|
|
||||||
switch n.Type {
|
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",
|
return fmt.Sprintf("%s %5d %5d %s %s %s%s",
|
||||||
mode|n.Mode, n.UID, n.GID, size,
|
mode|n.Mode, n.UID, n.GID, size,
|
||||||
n.ModTime.Local().Format(global.TimeFormat), path,
|
n.ModTime.Local().Format(global.TimeFormat), n.Path,
|
||||||
target)
|
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"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend"
|
"github.com/restic/restic/internal/backend"
|
||||||
"github.com/restic/restic/internal/backend/all"
|
"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/backend/retry"
|
||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
@@ -23,7 +24,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui/termstatus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -193,6 +194,7 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
|
|||||||
repository.TestUseLowSecurityKDFParameters(t)
|
repository.TestUseLowSecurityKDFParameters(t)
|
||||||
restic.TestDisableCheckPolynomial(t)
|
restic.TestDisableCheckPolynomial(t)
|
||||||
retry.TestFastRetries(t)
|
retry.TestFastRetries(t)
|
||||||
|
layout.TestDisablePackSubdirs(t)
|
||||||
|
|
||||||
tempdir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-")
|
tempdir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-")
|
||||||
rtest.OK(t, err)
|
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))
|
rtest.OK(t, os.MkdirAll(env.repo, 0700))
|
||||||
|
|
||||||
env.gopts = global.Options{
|
env.gopts = global.Options{
|
||||||
Repo: env.repo,
|
Repo: env.repo,
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
CacheDir: env.cache,
|
CacheDir: env.cache,
|
||||||
Password: rtest.TestPassword,
|
Password: rtest.TestPassword,
|
||||||
Extended: make(options.Options),
|
Extended: make(options.Options),
|
||||||
|
Compression: repository.CompressionFastest,
|
||||||
|
|
||||||
// replace this hook with "nil" if listing a filetype more than once is necessary
|
// 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 },
|
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 {
|
func listPacks(gopts global.Options, t *testing.T) restic.IDSet {
|
||||||
var packs restic.IDSet
|
var packs restic.IDSet
|
||||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
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 {
|
func listTreePacks(gopts global.Options, t *testing.T) restic.IDSet {
|
||||||
var treePacks restic.IDSet
|
var treePacks restic.IDSet
|
||||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
rtest.OK(t, r.LoadIndex(ctx, nil))
|
rtest.OK(t, r.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
|
||||||
treePacks = restic.NewIDSet()
|
treePacks = restic.NewIDSet()
|
||||||
return r.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
return r.ListBlobs(ctx, func(pb restic.PackBlob) {
|
||||||
if pb.Type == restic.TreeBlob {
|
if pb.Handle().Type == restic.TreeBlob {
|
||||||
treePacks.Insert(pb.PackID)
|
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) {
|
func removePacks(gopts global.Options, t testing.TB, remove restic.IDSet) {
|
||||||
be := captureBackend(&gopts)
|
be := captureBackend(&gopts)
|
||||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
ctx, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
for id := range remove {
|
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
|
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) {
|
func removePacksExcept(gopts global.Options, t testing.TB, keep restic.IDSet, removeTreePacks bool) {
|
||||||
be := captureBackend(&gopts)
|
be := captureBackend(&gopts)
|
||||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
ctx, r, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
// Get all tree packs
|
// Get all tree packs
|
||||||
rtest.OK(t, r.LoadIndex(ctx, nil))
|
rtest.OK(t, r.LoadIndex(ctx, restic.NoopTerminalCounterFactory))
|
||||||
|
|
||||||
treePacks := restic.NewIDSet()
|
treePacks := restic.NewIDSet()
|
||||||
rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackBlob) {
|
||||||
if pb.Type == restic.TreeBlob {
|
if pb.Handle().Type == restic.TreeBlob {
|
||||||
treePacks.Insert(pb.PackID)
|
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) {
|
if treePacks.Has(id) != removeTreePacks || keep.Has(id) {
|
||||||
return nil
|
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)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func includes(haystack []string, needle string) bool {
|
func includes(haystack []string, needle string) bool {
|
||||||
for _, s := range haystack {
|
return slices.Contains(haystack, needle)
|
||||||
if s == needle {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSnapshotMap(t testing.TB, gopts global.Options) map[string]struct{} {
|
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 {
|
func testLoadSnapshot(t testing.TB, gopts global.Options, id restic.ID) *data.Snapshot {
|
||||||
var snapshot *data.Snapshot
|
var snapshot *data.Snapshot
|
||||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
@@ -421,6 +418,13 @@ func withCaptureStdout(t testing.TB, gopts global.Options, callback func(ctx con
|
|||||||
return buf, err
|
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 {
|
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
|
// 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
|
// 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 {
|
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())
|
term, cancel := termstatus.Setup(stdin, stdout, stderr, gopts.Quiet)
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
term := termstatus.New(stdin, stdout, stderr, gopts.Quiet)
|
|
||||||
gopts.Term = term
|
gopts.Term = term
|
||||||
wg.Add(1)
|
defer cancel()
|
||||||
go func() {
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
defer wg.Done()
|
|
||||||
term.Run(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
defer wg.Wait()
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return callback(ctx, gopts)
|
return callback(ctx, gopts)
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import (
|
|||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckRestoreNoLock(t *testing.T) {
|
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)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -46,14 +48,14 @@ func TestCheckRestoreNoLock(t *testing.T) {
|
|||||||
// is expected by the first listing + some operations.
|
// is expected by the first listing + some operations.
|
||||||
type listOnceBackend struct {
|
type listOnceBackend struct {
|
||||||
backend.Backend
|
backend.Backend
|
||||||
listedFileType map[restic.FileType]bool
|
listedFileType map[backend.FileType]bool
|
||||||
strictOrder bool
|
strictOrder bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newListOnceBackend(be backend.Backend) *listOnceBackend {
|
func newListOnceBackend(be backend.Backend) *listOnceBackend {
|
||||||
return &listOnceBackend{
|
return &listOnceBackend{
|
||||||
Backend: be,
|
Backend: be,
|
||||||
listedFileType: make(map[restic.FileType]bool),
|
listedFileType: make(map[backend.FileType]bool),
|
||||||
strictOrder: false,
|
strictOrder: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,16 +63,16 @@ func newListOnceBackend(be backend.Backend) *listOnceBackend {
|
|||||||
func newOrderedListOnceBackend(be backend.Backend) *listOnceBackend {
|
func newOrderedListOnceBackend(be backend.Backend) *listOnceBackend {
|
||||||
return &listOnceBackend{
|
return &listOnceBackend{
|
||||||
Backend: be,
|
Backend: be,
|
||||||
listedFileType: make(map[restic.FileType]bool),
|
listedFileType: make(map[backend.FileType]bool),
|
||||||
strictOrder: true,
|
strictOrder: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (be *listOnceBackend) List(ctx context.Context, t restic.FileType, fn func(backend.FileInfo) error) error {
|
func (be *listOnceBackend) List(ctx context.Context, t backend.FileType, fn func(backend.FileInfo) error) error {
|
||||||
if t != restic.LockFile && be.listedFileType[t] {
|
if t != backend.LockFile && be.listedFileType[t] {
|
||||||
return errors.Errorf("tried listing type %v the second time", 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")
|
return errors.Errorf("tried listing type snapshots after index")
|
||||||
}
|
}
|
||||||
be.listedFileType[t] = true
|
be.listedFileType[t] = true
|
||||||
@@ -165,20 +167,32 @@ func TestFindListOnce(t *testing.T) {
|
|||||||
|
|
||||||
var snapshotIDs restic.IDSet
|
var snapshotIDs restic.IDSet
|
||||||
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
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)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
snapshotIDs = restic.NewIDSet()
|
snapshotIDs = restic.NewIDSet()
|
||||||
// specify the two oldest snapshots explicitly and use "latest" to reference the newest one
|
// specify the two oldest snapshots explicitly and use "latest" to reference the newest one
|
||||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &data.SnapshotFilter{}, []string{
|
err = (&data.SnapshotFilter{}).FindAll(ctx, repo, repo,
|
||||||
secondSnapshot[0].String(),
|
[]string{
|
||||||
secondSnapshot[1].String()[:8],
|
secondSnapshot[0].String(),
|
||||||
"latest",
|
secondSnapshot[1].String()[:8],
|
||||||
}, printer) {
|
"latest",
|
||||||
snapshotIDs.Insert(*sn.ID())
|
},
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -194,7 +208,7 @@ type failConfigOnceBackend struct {
|
|||||||
func (be *failConfigOnceBackend) Load(ctx context.Context, h backend.Handle,
|
func (be *failConfigOnceBackend) Load(ctx context.Context, h backend.Handle,
|
||||||
length int, offset int64, fn func(rd io.Reader) error) error {
|
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
|
be.failedOnce = true
|
||||||
return fmt.Errorf("oops")
|
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) {
|
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
|
be.failedOnce = true
|
||||||
return backend.FileInfo{}, fmt.Errorf("oops")
|
return backend.FileInfo{}, fmt.Errorf("oops")
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-10
@@ -5,10 +5,10 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"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)
|
repo, err := global.OpenRepository(ctx, gopts, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
@@ -16,9 +16,7 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
|
|||||||
|
|
||||||
unlock := func() {}
|
unlock := func() {}
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
var lock *repository.Unlocker
|
unlock, ctx, err = repository.LockRepo(ctx, repo, exclusive, gopts.RetryLock, func(msg string) {
|
||||||
|
|
||||||
lock, ctx, err = repository.Lock(ctx, repo, exclusive, gopts.RetryLock, func(msg string) {
|
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
printer.P("%s", msg)
|
printer.P("%s", msg)
|
||||||
}
|
}
|
||||||
@@ -26,8 +24,6 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
unlock = lock.Unlock
|
|
||||||
} else {
|
} else {
|
||||||
repo.SetDryRun()
|
repo.SetDryRun()
|
||||||
}
|
}
|
||||||
@@ -35,18 +31,18 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo
|
|||||||
return ctx, repo, unlock, nil
|
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
|
// 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
|
// As in-depth hardening, put the repository into read-only mode if noLock is true
|
||||||
// Not possible if the repository has to be locked.
|
// Not possible if the repository has to be locked.
|
||||||
return internalOpenWithLocked(ctx, gopts, noLock, false, printer)
|
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
|
// TODO enforce non-exclusive operations once the locking code has moved to the repository
|
||||||
return internalOpenWithLocked(ctx, gopts, dryRun, false, printer)
|
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)
|
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/feature"
|
||||||
"github.com/restic/restic/internal/global"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"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 {
|
PersistentPreRunE: func(c *cobra.Command, _ []string) error {
|
||||||
switch c.Name() {
|
switch c.Name() {
|
||||||
case "__complete", "__completeNoDesc":
|
case cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return globalOptions.PreRun(needsPassword(c.Name()))
|
return globalOptions.PreRun(needsPassword(c.Name()))
|
||||||
@@ -118,7 +117,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
|
|||||||
// user for authentication).
|
// user for authentication).
|
||||||
func needsPassword(cmd string) bool {
|
func needsPassword(cmd string) bool {
|
||||||
switch cmd {
|
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
|
return false
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
@@ -198,7 +197,7 @@ func main() {
|
|||||||
|
|
||||||
var exitMessage string
|
var exitMessage string
|
||||||
switch {
|
switch {
|
||||||
case restic.IsAlreadyLocked(err):
|
case repository.IsAlreadyLocked(err):
|
||||||
exitMessage = fmt.Sprintf("%v\nthe `unlock` command can be used to remove stale locks", err)
|
exitMessage = fmt.Sprintf("%v\nthe `unlock` command can be used to remove stale locks", err)
|
||||||
case err == ErrInvalidSourceData:
|
case err == ErrInvalidSourceData:
|
||||||
exitMessage = fmt.Sprintf("Warning: %v", err)
|
exitMessage = fmt.Sprintf("Warning: %v", err)
|
||||||
@@ -228,7 +227,7 @@ func main() {
|
|||||||
exitCode = 3
|
exitCode = 3
|
||||||
case errors.Is(err, global.ErrNoRepository):
|
case errors.Is(err, global.ErrNoRepository):
|
||||||
exitCode = 10
|
exitCode = 10
|
||||||
case restic.IsAlreadyLocked(err):
|
case repository.IsAlreadyLocked(err):
|
||||||
exitCode = 11
|
exitCode = 11
|
||||||
case errors.Is(err, repository.ErrNoKeyFound):
|
case errors.Is(err, repository.ErrNoKeyFound):
|
||||||
exitCode = 12
|
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``.
|
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.
|
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
|
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
|
matching environment variable for the word following a single ``$``. Note
|
||||||
that tilde (``~``) is not expanded, instead use the ``$HOME`` or equivalent
|
that tilde (``~``) is not expanded, instead use the ``$HOME`` or equivalent
|
||||||
environment variable (depending on your operating system).
|
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``:
|
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
|
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
|
or directories reside on. In other words, it will prevent restic from crossing
|
||||||
filesystem boundaries and subvolumes when performing a backup.
|
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
|
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``
|
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
|
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.
|
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
|
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
|
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
|
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
|
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.
|
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
|
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
|
disk space. Note that restic does not keep track of which files were originally
|
||||||
the original file, as their location is determined while restoring and is not
|
sparse or at what locations sparse holes were. When restoring, restic will create
|
||||||
stored explicitly.
|
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
|
Restoring extended file attributes
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|||||||
+3
-1
@@ -323,7 +323,7 @@ five days that had a snapshot and remove the other snapshots:
|
|||||||
keep 5 snapshots:
|
keep 5 snapshots:
|
||||||
ID Time Host Tags Reasons Paths
|
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
|
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
|
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
|
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``.
|
- ``--verbose`` increased verbosity shows additional statistics for ``prune``.
|
||||||
|
|
||||||
|
- ``--json`` gives the statistics in JSON format.
|
||||||
|
|
||||||
|
|
||||||
Recovering from "no free space" errors
|
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_PROGRESS_FPS Frames per second by which the progress bar is updated
|
||||||
RESTIC_PACK_SIZE Target size for pack files
|
RESTIC_PACK_SIZE Target size for pack files
|
||||||
RESTIC_READ_CONCURRENCY Concurrency for file reads
|
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 Source repository for copy (replaces --from-repo)
|
||||||
RESTIC_FROM_REPOSITORY_FILE File containing source repository for copy (replaces --from-repository-file)
|
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
|
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. 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
|
backup
|
||||||
------
|
------
|
||||||
|
|
||||||
@@ -311,6 +320,15 @@ Verbose status provides details about the progress, including details about back
|
|||||||
| ``total_files`` | Total number of files | uint64 |
|
| ``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
|
Summary
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
||||||
@@ -534,8 +552,7 @@ forget
|
|||||||
The ``forget`` command prints a single JSON document containing an array of
|
The ``forget`` command prints a single JSON document containing an array of
|
||||||
ForgetGroups. If specific snapshot IDs are specified, then no output is generated.
|
ForgetGroups. If specific snapshot IDs are specified, then no output is generated.
|
||||||
|
|
||||||
The ``prune`` command does not yet support JSON such that ``forget --prune``
|
When using the ``--prune`` option, its output is appended in the JSON lines format.
|
||||||
results in a mix of JSON and text output.
|
|
||||||
|
|
||||||
ForgetGroup
|
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
|
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
|
somewhere. Please include the check output and additional information that might
|
||||||
help locate the problem.
|
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
|
them using the ``repair packs`` command. Use that command instead of the "Repairing the
|
||||||
index" section in this guide.
|
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
|
use the standard snapshot filter method specifying ``--host``, ``--path``, ``--tag`` or
|
||||||
alternatively naming snapshot ID(s) explicitly. The selected subset of packfiles
|
alternatively naming snapshot ID(s) explicitly. The selected subset of packfiles
|
||||||
will then be checked for consistency and read when either ``--read-data`` or
|
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
|
Please note that it is not recommended to repair the index unless the repository
|
||||||
is actually damaged.
|
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
|
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``.
|
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.
|
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``
|
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
|
modified snapshots using the ``forget`` command. In the example above, you'd have
|
||||||
to run ``restic forget 6979421e``.
|
to run ``restic forget 6979421e``.
|
||||||
|
|
||||||
|
7. Checking the repository again
|
||||||
6. Checking the repository again
|
|
||||||
********************************
|
********************************
|
||||||
|
|
||||||
As a final step, run ``check`` again to make sure that the repository has been successfully
|
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,
|
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.
|
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()``.
|
If you want to analyze JSON output, you use ``withCaptureStdout()``.
|
||||||
It returns the generated output in a ``*bytes.Buffer``.
|
It returns the generated output in a ``*bytes.Buffer``.
|
||||||
JSON output can be unmarshalled to produce the appropriate go structures; see
|
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
|
possible critical security problem, please do *not* open a GitHub issue
|
||||||
but send an email directly to alexander@bumpern.de. If possible, please
|
but send an email directly to alexander@bumpern.de. If possible, please
|
||||||
encrypt your email using the following PGP key
|
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:
|
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
|
- 2021-04-02: `The Changelog: Restic has your backup
|
||||||
(Podcast) <https://changelog.com/podcast/434>`__
|
(Podcast) <https://changelog.com/podcast/434>`__
|
||||||
- 2016-01-31: Lightning Talk at the Go Devroom at FOSDEM 2016,
|
- 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
|
unique amongst all the other files in the same directory, the prefix may
|
||||||
be used instead of the complete filename.
|
be used instead of the complete filename.
|
||||||
|
|
||||||
Apart from the files stored within the ``keys`` and ``data`` directories,
|
Apart from the files stored within the ``keys`` directory, all files
|
||||||
all files are encrypted with AES-256 in counter mode (CTR). The integrity
|
are encrypted with AES-256 in counter mode (CTR). The integrity of the
|
||||||
of the encrypted data is secured by a Poly1305-AES message authentication
|
encrypted data is secured by a
|
||||||
code (MAC).
|
Poly1305-AES message authentication code (MAC).
|
||||||
Files in the ``data`` directory ("pack files") consist of multiple parts
|
Files in the ``data`` directory ("pack files") consist of multiple
|
||||||
which are all independently encrypted and authenticated, see below.
|
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
|
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
|
(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:
|
- Currently, only the following commands are known to work:
|
||||||
|
|
||||||
- ``backup``
|
- ``backup``
|
||||||
|
- ``check``
|
||||||
- ``copy``
|
- ``copy``
|
||||||
- ``prune``
|
- ``prune``
|
||||||
- ``restore``
|
- ``restore``
|
||||||
|
|||||||
+14
-1
@@ -30,7 +30,8 @@ The allowed types are (in alphabetic order):
|
|||||||
- :ref:`list_index`
|
- :ref:`list_index`
|
||||||
- :ref:`keys <list_keys_locks_packs>`
|
- :ref:`keys <list_keys_locks_packs>`
|
||||||
- :ref:`locks <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`
|
- :ref:`list_snapshots`
|
||||||
|
|
||||||
|
|
||||||
@@ -160,6 +161,18 @@ Here is an example which lists all the packs in the repository:
|
|||||||
953e5381138bdc44da23740a83065809dd4021f45ce4e351b577dc4c07f81314
|
953e5381138bdc44da23740a83065809dd4021f45ce4e351b577dc4c07f81314
|
||||||
75bca8556f47d16362e58e757ea89a34b28fb96aedcc314bea35d468e5cb665c
|
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:
|
.. _view-repository-objects:
|
||||||
|
|
||||||
Inspecting repository objects
|
Inspecting repository objects
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ toolchain go1.25.10
|
|||||||
godebug winsymlink=0
|
godebug winsymlink=0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/storage v1.62.2
|
cloud.google.com/go/storage v1.64.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0
|
||||||
github.com/Backblaze/blazer v0.7.2
|
github.com/Backblaze/blazer v0.7.2
|
||||||
github.com/Microsoft/go-winio v0.6.2
|
github.com/Microsoft/go-winio v0.6.2
|
||||||
github.com/anacrolix/fuse v0.3.1
|
github.com/anacrolix/fuse v0.3.1
|
||||||
@@ -22,42 +22,42 @@ require (
|
|||||||
github.com/go-ole/go-ole v1.3.0
|
github.com/go-ole/go-ole v1.3.0
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||||
github.com/klauspost/compress v1.18.6
|
github.com/klauspost/compress v1.18.7
|
||||||
github.com/minio/minio-go/v7 v7.1.0
|
github.com/minio/minio-go/v7 v7.2.1
|
||||||
github.com/ncw/swift/v2 v2.0.5
|
github.com/ncw/swift/v2 v2.0.5
|
||||||
github.com/peterbourgon/unixtransport v0.0.7
|
github.com/peterbourgon/unixtransport v0.0.7
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkg/profile v1.7.0
|
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/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/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
go.uber.org/automaxprocs v1.6.0
|
go.uber.org/automaxprocs v1.6.0
|
||||||
golang.org/x/crypto v0.52.0
|
golang.org/x/crypto v0.54.0
|
||||||
golang.org/x/net v0.55.0
|
golang.org/x/net v0.57.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.22.0
|
||||||
golang.org/x/sys v0.45.0
|
golang.org/x/sys v0.47.0
|
||||||
golang.org/x/term v0.43.0
|
golang.org/x/term v0.45.0
|
||||||
golang.org/x/text v0.37.0
|
golang.org/x/text v0.40.0
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
google.golang.org/api v0.282.0
|
google.golang.org/api v0.291.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cel.dev/expr v0.25.1 // indirect
|
cel.dev/expr v0.25.1 // indirect
|
||||||
cloud.google.com/go v0.123.0 // 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/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
cloud.google.com/go/iam v1.7.0 // indirect
|
cloud.google.com/go/iam v1.11.0 // indirect
|
||||||
cloud.google.com/go/monitoring v1.24.3 // 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/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/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/detectors/gcp v1.32.0 // indirect
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.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/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // 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/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||||
github.com/felixge/fgprof v0.9.3 // indirect
|
github.com/felixge/fgprof v0.9.3 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // 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-jose/go-jose/v4 v4.1.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // 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/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||||
@@ -91,18 +90,19 @@ require (
|
|||||||
github.com/tinylib/msgp v1.6.1 // indirect
|
github.com/tinylib/msgp v1.6.1 // indirect
|
||||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
|
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.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/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // indirect
|
||||||
google.golang.org/grpc v1.81.1 // indirect
|
google.golang.org/grpc v1.82.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // 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=
|
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 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
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.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ=
|
||||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
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 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
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 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
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.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
|
||||||
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
|
cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
|
||||||
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
|
cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k=
|
||||||
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
|
cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI=
|
||||||
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
|
cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
|
||||||
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
|
cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
|
||||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY=
|
||||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM=
|
||||||
cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4=
|
cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg=
|
||||||
cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
|
cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ=
|
||||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
|
||||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
|
||||||
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.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||||
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/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||||
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.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
|
||||||
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 v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
|
||||||
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.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
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 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/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 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/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.8.0 h1:irsmOWwkp0KCTTNS5e2hdFeIvSQClQo2No3IaNmL3Vw=
|
||||||
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/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 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
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 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
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 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM=
|
||||||
github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10=
|
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.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
|
||||||
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.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
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.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
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/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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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/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 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
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=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
|
||||||
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
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 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
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=
|
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/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 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
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.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.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 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
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/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 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
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.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
|
||||||
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
|
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 h1:9o5Gsd7bInAFEqsGPcaUdsboMbqf8lnNtxqWKFT9iz8=
|
||||||
github.com/ncw/swift/v2 v2.0.5/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
|
github.com/ncw/swift/v2 v2.0.5/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
|
||||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
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/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 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
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.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
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 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=
|
||||||
github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
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=
|
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/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 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
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.5.0 h1:1y+ut0MBduzxODJ298rhQCtESoEpj8v1hTydZlKaE1Y=
|
||||||
github.com/restic/chunker v0.4.0/go.mod h1:z0cH2BejpW636LXw0R/BGyv+Ey8+m9QGiOanDHItzyw=
|
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/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.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
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.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.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.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.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.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.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.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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
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=
|
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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
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.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
|
||||||
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.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
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 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
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.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
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 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
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-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-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.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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
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 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
|
||||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
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=
|
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-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.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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
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 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
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-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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
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-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.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
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 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
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=
|
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=
|
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 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
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.291.0 h1:wfPbbY+mr9c7wZLqqzrHJLft/q8iFKREd6IgTBUene0=
|
||||||
google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM=
|
google.golang.org/api v0.291.0/go.mod h1:at7kwWbuonglBFEBoeMDAV1bguHqL3qf0BHFsv3coa0=
|
||||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 h1:YJjbgu+dkp5kUJLfpMyCLfBIWZb/FcJyuLeo1gVBOuo=
|
||||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
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-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
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 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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/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.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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func init() {
|
|||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
func die(f string, args ...interface{}) {
|
func die(f string, args ...any) {
|
||||||
if !strings.HasSuffix(f, "\n") {
|
if !strings.HasSuffix(f, "\n") {
|
||||||
f += "\n"
|
f += "\n"
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ func die(f string, args ...interface{}) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func msg(f string, args ...interface{}) {
|
func msg(f string, args ...any) {
|
||||||
if !strings.HasSuffix(f, "\n") {
|
if !strings.HasSuffix(f, "\n") {
|
||||||
f += "\n"
|
f += "\n"
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ func msg(f string, args ...interface{}) {
|
|||||||
fmt.Printf(f, args...)
|
fmt.Printf(f, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func verbose(f string, args ...interface{}) {
|
func verbose(f string, args ...any) {
|
||||||
if !opts.Verbose {
|
if !opts.Verbose {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -207,10 +207,7 @@ func buildForTarget(sourceDir, outputDir, goos, goarch string) (filename string)
|
|||||||
func buildTargets(sourceDir, outputDir string, targets map[string][]string) {
|
func buildTargets(sourceDir, outputDir string, targets map[string][]string) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
// the go compiler is already parallelized, thus reduce the concurrency a bit
|
// the go compiler is already parallelized, thus reduce the concurrency a bit
|
||||||
workers := runtime.GOMAXPROCS(0) / 4
|
workers := max(runtime.GOMAXPROCS(0)/4, 1)
|
||||||
if workers < 1 {
|
|
||||||
workers = 1
|
|
||||||
}
|
|
||||||
msg("building with %d workers", workers)
|
msg("building with %d workers", workers)
|
||||||
|
|
||||||
type Job struct{ GOOS, GOARCH string }
|
type Job struct{ GOOS, GOARCH string }
|
||||||
@@ -218,7 +215,7 @@ func buildTargets(sourceDir, outputDir string, targets map[string][]string) {
|
|||||||
var wg errgroup.Group
|
var wg errgroup.Group
|
||||||
ch := make(chan Job)
|
ch := make(chan Job)
|
||||||
|
|
||||||
for i := 0; i < workers; i++ {
|
for range workers {
|
||||||
wg.Go(func() error {
|
wg.Go(func() error {
|
||||||
for job := range ch {
|
for job := range ch {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func init() {
|
|||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
func die(f string, args ...interface{}) {
|
func die(f string, args ...any) {
|
||||||
if !strings.HasSuffix(f, "\n") {
|
if !strings.HasSuffix(f, "\n") {
|
||||||
f += "\n"
|
f += "\n"
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func die(f string, args ...interface{}) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func msg(f string, args ...interface{}) {
|
func msg(f string, args ...any) {
|
||||||
if !strings.HasSuffix(f, "\n") {
|
if !strings.HasSuffix(f, "\n") {
|
||||||
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)
|
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")
|
run("sh", "-c", buildCmd+" --no-cache")
|
||||||
|
|
||||||
publishCmds := ""
|
var publishCmds strings.Builder
|
||||||
for _, tag := range []string{"restic/restic:latest", "restic/restic:" + version} {
|
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 {
|
func tempdir(prefix string) string {
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ func (s *ItemStats) Add(other ItemStats) {
|
|||||||
s.TreeSizeInRepo += other.TreeSizeInRepo
|
s.TreeSizeInRepo += other.TreeSizeInRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToNoder returns a data.Node for a File.
|
// toNoder returns a data.Node for a File.
|
||||||
type ToNoder interface {
|
type toNoder interface {
|
||||||
ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*data.Node, error)
|
ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*data.Node, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ type archiverRepo interface {
|
|||||||
restic.WithBlobUploader
|
restic.WithBlobUploader
|
||||||
restic.SaverUnpacked[restic.WriteableFileType]
|
restic.SaverUnpacked[restic.WriteableFileType]
|
||||||
|
|
||||||
Config() restic.Config
|
ChunkerFactory() restic.ChunkerFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archiver saves a directory structure to the repo.
|
// Archiver saves a directory structure to the repo.
|
||||||
@@ -110,11 +110,11 @@ type Archiver struct {
|
|||||||
// particular file/dir.
|
// particular file/dir.
|
||||||
//
|
//
|
||||||
// Once reading a file has completed successfully (but not saving it yet),
|
// 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
|
// CompleteItem may be called asynchronously from several different
|
||||||
// goroutines!
|
// 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 is called when a file is being processed by a worker.
|
||||||
StartFile func(filename string)
|
StartFile func(filename string)
|
||||||
@@ -129,6 +129,9 @@ type Archiver struct {
|
|||||||
|
|
||||||
// Flags controlling change detection. See doc/040_backup.rst for details.
|
// Flags controlling change detection. See doc/040_backup.rst for details.
|
||||||
ChangeIgnoreFlags uint
|
ChangeIgnoreFlags uint
|
||||||
|
|
||||||
|
// for excluded items
|
||||||
|
ExcludedItem func(path string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flags for the ChangeIgnoreFlags bitfield.
|
// Flags for the ChangeIgnoreFlags bitfield.
|
||||||
@@ -149,9 +152,9 @@ type Options struct {
|
|||||||
SaveTreeConcurrency uint
|
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.
|
// fields.
|
||||||
func (o Options) ApplyDefaults() Options {
|
func (o Options) applyDefaults() Options {
|
||||||
if o.ReadConcurrency == 0 {
|
if o.ReadConcurrency == 0 {
|
||||||
// two is a sweet spot for almost all situations. We've done some
|
// two is a sweet spot for almost all situations. We've done some
|
||||||
// experiments documented here:
|
// experiments documented here:
|
||||||
@@ -178,11 +181,12 @@ func New(repo archiverRepo, filesystem fs.FS, opts Options) *Archiver {
|
|||||||
SelectByName: func(_ string) bool { return true },
|
SelectByName: func(_ string) bool { return true },
|
||||||
Select: func(_ string, _ *fs.ExtendedFileInfo, _ fs.FS) bool { return true },
|
Select: func(_ string, _ *fs.ExtendedFileInfo, _ fs.FS) bool { return true },
|
||||||
FS: filesystem,
|
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) {},
|
StartFile: func(string) {},
|
||||||
CompleteBlob: func(uint64) {},
|
CompleteBlob: func(uint64) {},
|
||||||
|
ExcludedItem: func(string) {},
|
||||||
}
|
}
|
||||||
|
|
||||||
return arch
|
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) {
|
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()
|
arch.mu.Lock()
|
||||||
defer arch.mu.Unlock()
|
defer arch.mu.Unlock()
|
||||||
|
|
||||||
arch.summary.ItemStats.Add(s)
|
arch.summary.ItemStats.Add(s)
|
||||||
|
|
||||||
if current != nil {
|
if action.IsZero() {
|
||||||
arch.summary.ProcessedBytes += current.Size
|
|
||||||
} else {
|
|
||||||
// last item or an error occurred
|
// last item or an error occurred
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch current.Type {
|
arch.summary.ProcessedBytes += current.Size
|
||||||
case data.NodeTypeDir:
|
|
||||||
switch {
|
|
||||||
case previous == nil:
|
|
||||||
arch.summary.Dirs.New++
|
|
||||||
case previous.Equals(*current):
|
|
||||||
arch.summary.Dirs.Unchanged++
|
|
||||||
default:
|
|
||||||
arch.summary.Dirs.Changed++
|
|
||||||
}
|
|
||||||
|
|
||||||
case data.NodeTypeFile:
|
switch action {
|
||||||
switch {
|
case ActionDirNew:
|
||||||
case previous == nil:
|
arch.summary.Dirs.New++
|
||||||
arch.summary.Files.New++
|
case ActionDirUnchanged:
|
||||||
case previous.Equals(*current):
|
arch.summary.Dirs.Unchanged++
|
||||||
arch.summary.Files.Unchanged++
|
case ActionDirModified:
|
||||||
default:
|
arch.summary.Dirs.Changed++
|
||||||
arch.summary.Files.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.
|
// 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) {
|
node, err := meta.ToNode(ignoreXattrListError, func(format string, args ...any) {
|
||||||
_ = arch.error(filename, fmt.Errorf(format, args...))
|
_ = 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 {
|
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)
|
err = errors.Errorf("tree %v could not be loaded; the repository could be damaged: %v", id, err)
|
||||||
} else {
|
} else {
|
||||||
err = errors.Errorf("tree %v is not known; the repository could be damaged, run `repair index` to try to repair it", id)
|
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 {
|
func (arch *Archiver) allBlobsPresent(previous *data.Node) bool {
|
||||||
// check if all blobs are contained in index
|
// check if all blobs are contained in index
|
||||||
for _, id := range previous.Content {
|
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
|
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
|
// exclude files by path before running Lstat to reduce number of lstat calls
|
||||||
if !explicit && !arch.SelectByName(abstarget) {
|
if !explicit && !arch.SelectByName(abstarget) {
|
||||||
debug.Log("%v is excluded by path", target)
|
debug.Log("%v is excluded by path", target)
|
||||||
|
arch.ExcludedItem(abstarget)
|
||||||
return futureNode{}, true, nil
|
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) {
|
if !explicit && !arch.Select(abstarget, fi, arch.FS) {
|
||||||
debug.Log("%v is excluded", target)
|
debug.Log("%v is excluded", target)
|
||||||
|
arch.ExcludedItem(abstarget)
|
||||||
return futureNode{}, true, nil
|
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
|
// Paths returned with Explicit true are those the user listed literally; paths
|
||||||
// inserted from directory expansion have Explicit false.
|
// 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)
|
debug.Log("targets before resolving: %v", targets)
|
||||||
result := make([]BackupTarget, 0, len(targets))
|
result := make([]backupTarget, 0, len(targets))
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
if target != "" && filesys.VolumeName(target) == target {
|
if target != "" && filesys.VolumeName(target) == target {
|
||||||
// special case to allow users to also specify a volume name "C:" instead of a path "C:\"
|
// 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)
|
pc, _ := pathComponents(filesys, target, false)
|
||||||
if len(pc) > 0 {
|
if len(pc) > 0 {
|
||||||
result = append(result, BackupTarget{Path: target, Explicit: true})
|
result = append(result, backupTarget{Path: target, Explicit: true})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,7 +815,7 @@ func resolveRelativeTargets(filesys fs.FS, targets []string) ([]BackupTarget, er
|
|||||||
sort.Strings(entries)
|
sort.Strings(entries)
|
||||||
|
|
||||||
for _, name := range entries {
|
for _, name := range entries {
|
||||||
result = append(result, BackupTarget{
|
result = append(result, backupTarget{
|
||||||
Path: filesys.Join(target, name),
|
Path: filesys.Join(target, name),
|
||||||
Explicit: false,
|
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) {
|
func (arch *Archiver) runWorkers(ctx context.Context, wg *errgroup.Group, uploader restic.BlobSaverAsync) {
|
||||||
arch.fileSaver = newFileSaver(ctx, wg,
|
arch.fileSaver = newFileSaver(ctx, wg,
|
||||||
uploader,
|
uploader,
|
||||||
arch.Repo.Config().ChunkerPolynomial,
|
arch.Repo.ChunkerFactory(),
|
||||||
arch.Options.ReadConcurrency)
|
arch.Options.ReadConcurrency)
|
||||||
arch.fileSaver.CompleteBlob = arch.CompleteBlob
|
arch.fileSaver.CompleteBlob = arch.CompleteBlob
|
||||||
arch.fileSaver.NodeFromFileInfo = arch.nodeFromFileInfo
|
arch.fileSaver.NodeFromFileInfo = arch.nodeFromFileInfo
|
||||||
|
|||||||
@@ -132,11 +132,10 @@ func TestArchiverSaveFile(t *testing.T) {
|
|||||||
|
|
||||||
for _, testfile := range tests {
|
for _, testfile := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, TestDir{"file": testfile})
|
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)
|
TestEnsureFileContent(ctx, t, repo, "file", node, testfile)
|
||||||
if stats.DataSize != uint64(len(testfile.Content)) {
|
if stats.DataSize != uint64(len(testfile.Content)) {
|
||||||
@@ -165,8 +164,7 @@ func TestArchiverSaveFileReaderFS(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
repo := repository.TestRepository(t)
|
repo := repository.TestRepository(t)
|
||||||
|
|
||||||
@@ -206,12 +204,11 @@ func TestArchiverSave(t *testing.T) {
|
|||||||
|
|
||||||
for _, testfile := range tests {
|
for _, testfile := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, TestDir{"file": testfile})
|
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 {
|
arch.Error = func(item string, err error) error {
|
||||||
t.Errorf("archiver error for %v: %v", item, err)
|
t.Errorf("archiver error for %v: %v", item, err)
|
||||||
return err
|
return err
|
||||||
@@ -276,8 +273,7 @@ func TestArchiverSaveReaderFS(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
repo := repository.TestRepository(t)
|
repo := repository.TestRepository(t)
|
||||||
|
|
||||||
@@ -357,7 +353,7 @@ func BenchmarkArchiverSaveFileSmall(b *testing.B) {
|
|||||||
tempdir, repo := prepareTempdirRepoSrc(b, d)
|
tempdir, repo := prepareTempdirRepoSrc(b, d)
|
||||||
b.StartTimer()
|
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()
|
b.StopTimer()
|
||||||
if stats.DataSize != fileSize {
|
if stats.DataSize != fileSize {
|
||||||
@@ -389,7 +385,7 @@ func BenchmarkArchiverSaveFileLarge(b *testing.B) {
|
|||||||
tempdir, repo := prepareTempdirRepoSrc(b, d)
|
tempdir, repo := prepareTempdirRepoSrc(b, d)
|
||||||
b.StartTimer()
|
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()
|
b.StopTimer()
|
||||||
if stats.DataSize != fileSize {
|
if stats.DataSize != fileSize {
|
||||||
@@ -477,9 +473,9 @@ func TestArchiverSaveFileIncremental(t *testing.T) {
|
|||||||
data := rtest.Random(23, 512*1024+887898)
|
data := rtest.Random(23, 512*1024+887898)
|
||||||
testfile := filepath.Join(tempdir, "testfile")
|
testfile := filepath.Join(tempdir, "testfile")
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
appendToFile(t, testfile, data)
|
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)
|
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.
|
// sleep sleeps long enough to ensure a timestamp change.
|
||||||
func sleep() {
|
func sleep() {
|
||||||
d := 50 * time.Millisecond
|
time.Sleep(5 * 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFileChanged(t *testing.T) {
|
func TestFileChanged(t *testing.T) {
|
||||||
@@ -691,7 +681,7 @@ func TestFileChanged(t *testing.T) {
|
|||||||
}
|
}
|
||||||
save(t, filename, content)
|
save(t, filename, content)
|
||||||
|
|
||||||
fs := &fs.Local{}
|
fs := fs.NewLocal()
|
||||||
fiBefore, err := fs.Lstat(filename)
|
fiBefore, err := fs.Lstat(filename)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
node := nodeFromFile(t, fs, filename)
|
node := nodeFromFile(t, fs, filename)
|
||||||
@@ -735,7 +725,7 @@ func TestFilChangedSpecialCases(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("type-change", func(t *testing.T) {
|
t.Run("type-change", func(t *testing.T) {
|
||||||
fi := lstat(t, filename)
|
fi := lstat(t, filename)
|
||||||
node := nodeFromFile(t, &fs.Local{}, filename)
|
node := nodeFromFile(t, fs.NewLocal(), filename)
|
||||||
node.Type = data.NodeTypeSymlink
|
node.Type = data.NodeTypeSymlink
|
||||||
if !fileChanged(fi, node, 0) {
|
if !fileChanged(fi, node, 0) {
|
||||||
t.Fatal("node with changed type detected as unchanged")
|
t.Fatal("node with changed type detected as unchanged")
|
||||||
@@ -754,12 +744,12 @@ func TestArchiverSaveDir(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
src: TestDir{
|
src: TestDir{
|
||||||
"targetfile": TestFile{Content: string(rtest.Random(888, 2*1024*1024+5000))},
|
"targetfile": TestFile{Content: string(rtest.Random(888, 20*1024+5000))},
|
||||||
},
|
},
|
||||||
target: ".",
|
target: ".",
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
"targetdir": 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"},
|
"foo": TestFile{Content: "foo"},
|
||||||
"emptyfile": TestFile{Content: ""},
|
"emptyfile": TestFile{Content: ""},
|
||||||
"bar": TestFile{Content: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
|
"bar": TestFile{Content: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
|
||||||
"largefile": TestFile{Content: string(rtest.Random(888, 2*1024*1024+5000))},
|
"largefile": TestFile{Content: string(rtest.Random(888, 1*1024*1024+5000))},
|
||||||
"largerfile": TestFile{Content: string(rtest.Random(234, 5*1024*1024+5000))},
|
"largerfile": TestFile{Content: string(rtest.Random(234, 3*1024*1024+5000))},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
target: "targetdir",
|
target: "targetdir",
|
||||||
@@ -837,7 +827,7 @@ func TestArchiverSaveDir(t *testing.T) {
|
|||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||||
|
|
||||||
testFS := fs.Track{FS: fs.Local{}}
|
testFS := fs.Track{FS: fs.NewLocal()}
|
||||||
arch := New(repo, testFS, Options{})
|
arch := New(repo, testFS, Options{})
|
||||||
arch.summary = &Summary{}
|
arch.summary = &Summary{}
|
||||||
|
|
||||||
@@ -931,7 +921,7 @@ func TestArchiverSaveDirDuplicateExcludedEntry(t *testing.T) {
|
|||||||
tempdir, repo := prepareTempdirRepoSrc(t, src)
|
tempdir, repo := prepareTempdirRepoSrc(t, src)
|
||||||
|
|
||||||
testFS := fs.Track{FS: &duplicateReaddirFS{
|
testFS := fs.Track{FS: &duplicateReaddirFS{
|
||||||
FS: &fs.Local{},
|
FS: fs.NewLocal(),
|
||||||
dir: ".",
|
dir: ".",
|
||||||
names: []string{"excluded", "excluded", "keep"},
|
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
|
// save the empty directory several times in a row, then have a look if the
|
||||||
// archiver did save the same tree several times
|
// archiver did save the same tree several times
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
testFS := fs.Track{FS: fs.Local{}}
|
testFS := fs.Track{FS: fs.NewLocal()}
|
||||||
arch := New(repo, testFS, Options{})
|
arch := New(repo, testFS, Options{})
|
||||||
arch.summary = &Summary{}
|
arch.summary = &Summary{}
|
||||||
|
|
||||||
@@ -1173,7 +1163,7 @@ func TestArchiverSaveTree(t *testing.T) {
|
|||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||||
|
|
||||||
testFS := fs.Track{FS: fs.Local{}}
|
testFS := fs.Track{FS: fs.NewLocal()}
|
||||||
|
|
||||||
arch := New(repo, testFS, Options{})
|
arch := New(repo, testFS, Options{})
|
||||||
arch.summary = &Summary{}
|
arch.summary = &Summary{}
|
||||||
@@ -1467,12 +1457,11 @@ func TestArchiverSnapshot(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
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
|
chdir := tempdir
|
||||||
if test.chdir != "" {
|
if test.chdir != "" {
|
||||||
@@ -1571,7 +1560,7 @@ func TestResolveRelativeTargetsSpecial(t *testing.T) {
|
|||||||
t.Skip("skip test on unix")
|
t.Skip("skip test on unix")
|
||||||
}
|
}
|
||||||
|
|
||||||
targets, err := resolveRelativeTargets(&fs.Local{}, test.targets)
|
targets, err := resolveRelativeTargets(fs.NewLocal(), test.targets)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
paths := make([]string, len(targets))
|
paths := make([]string, len(targets))
|
||||||
for i, tgt := range targets {
|
for i, tgt := range targets {
|
||||||
@@ -1688,12 +1677,11 @@ func TestArchiverSnapshotSelect(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
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
|
arch.Select = test.selFn
|
||||||
|
|
||||||
back := rtest.Chdir(t, tempdir)
|
back := rtest.Chdir(t, tempdir)
|
||||||
@@ -1796,12 +1784,11 @@ func TestArchiverExplicitBackupTarget(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
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
|
arch.Select = test.selFn
|
||||||
|
|
||||||
back := rtest.Chdir(t, tempdir)
|
back := rtest.Chdir(t, tempdir)
|
||||||
@@ -1962,13 +1949,12 @@ func TestArchiverParent(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||||
|
|
||||||
testFS := &MockFS{
|
testFS := &MockFS{
|
||||||
FS: fs.Track{FS: fs.Local{}},
|
FS: fs.Track{FS: fs.NewLocal()},
|
||||||
bytesRead: make(map[string]int),
|
bytesRead: make(map[string]int),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1988,7 +1974,7 @@ func TestArchiverParent(t *testing.T) {
|
|||||||
t.Logf("testfs: %v", testFS)
|
t.Logf("testfs: %v", testFS)
|
||||||
|
|
||||||
// check that all files have been read exactly once
|
// 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)
|
file, ok := item.(TestFile)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
@@ -2150,8 +2136,7 @@ func TestArchiverErrorReporting(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||||
|
|
||||||
@@ -2162,7 +2147,7 @@ func TestArchiverErrorReporting(t *testing.T) {
|
|||||||
test.prepare(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
|
arch.Error = test.errFn
|
||||||
|
|
||||||
target := test.targets
|
target := test.targets
|
||||||
@@ -2243,7 +2228,7 @@ func TestArchiverContextCanceled(t *testing.T) {
|
|||||||
back := rtest.Chdir(t, tempdir)
|
back := rtest.Chdir(t, tempdir)
|
||||||
defer back()
|
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()})
|
_, snapshotID, _, err := arch.Snapshot(ctx, []string{"."}, SnapshotOptions{Time: time.Now()})
|
||||||
|
|
||||||
@@ -2376,8 +2361,7 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
tempdir, repo := prepareTempdirRepoSrc(t, test.src)
|
||||||
|
|
||||||
@@ -2385,7 +2369,7 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
|
|||||||
defer back()
|
defer back()
|
||||||
|
|
||||||
testFS := &TrackFS{
|
testFS := &TrackFS{
|
||||||
FS: fs.Track{FS: fs.Local{}},
|
FS: fs.Track{FS: fs.NewLocal()},
|
||||||
opened: make(map[string]uint),
|
opened: make(map[string]uint),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2518,7 +2502,7 @@ func TestMetadataChanged(t *testing.T) {
|
|||||||
|
|
||||||
// get metadata
|
// get metadata
|
||||||
fi := lstat(t, "testfile")
|
fi := lstat(t, "testfile")
|
||||||
localFS := &fs.Local{}
|
localFS := fs.NewLocal()
|
||||||
meta, err := localFS.OpenFile("testfile", fs.O_NOFOLLOW, true)
|
meta, err := localFS.OpenFile("testfile", fs.O_NOFOLLOW, true)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
want, err := meta.ToNode(false, t.Logf)
|
want, err := meta.ToNode(false, t.Logf)
|
||||||
@@ -2619,13 +2603,12 @@ func TestRacyFileTypeSwap(t *testing.T) {
|
|||||||
tempfile := filepath.Join(tempdir, realName)
|
tempfile := filepath.Join(tempdir, realName)
|
||||||
|
|
||||||
statfs := &overrideFS{
|
statfs := &overrideFS{
|
||||||
FS: fs.Local{},
|
FS: fs.NewLocal(),
|
||||||
overrideFI: fakeFI,
|
overrideFI: fakeFI,
|
||||||
resetFIOnRead: true,
|
resetFIOnRead: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_ = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
_ = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||||
wg, ctx := errgroup.WithContext(ctx)
|
wg, ctx := errgroup.WithContext(ctx)
|
||||||
@@ -2666,7 +2649,7 @@ func TestMetadataBackupErrorFiltering(t *testing.T) {
|
|||||||
filename := filepath.Join(tempdir, "file")
|
filename := filepath.Join(tempdir, "file")
|
||||||
repo := repository.TestRepository(t)
|
repo := repository.TestRepository(t)
|
||||||
|
|
||||||
arch := New(repo, fs.Local{}, Options{})
|
arch := New(repo, fs.NewLocal(), Options{})
|
||||||
|
|
||||||
var filteredErr error
|
var filteredErr error
|
||||||
replacementErr := fmt.Errorf("replacement")
|
replacementErr := fmt.Errorf("replacement")
|
||||||
@@ -2715,7 +2698,7 @@ func TestIrregularFile(t *testing.T) {
|
|||||||
fi.Mode = (fi.Mode &^ os.ModeType) | os.ModeIrregular
|
fi.Mode = (fi.Mode &^ os.ModeType) | os.ModeIrregular
|
||||||
|
|
||||||
override := &overrideFS{
|
override := &overrideFS{
|
||||||
FS: fs.Local{},
|
FS: fs.NewLocal(),
|
||||||
overrideFI: fi,
|
overrideFI: fi,
|
||||||
overrideNode: &data.Node{
|
overrideNode: &data.Node{
|
||||||
Type: data.NodeTypeIrregular,
|
Type: data.NodeTypeIrregular,
|
||||||
@@ -2723,8 +2706,7 @@ func TestIrregularFile(t *testing.T) {
|
|||||||
overrideErr: fmt.Errorf(`unsupported file type "irregular"`),
|
overrideErr: fmt.Errorf(`unsupported file type "irregular"`),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
arch := New(repo, fs.Track{FS: override}, Options{})
|
arch := New(repo, fs.Track{FS: override}, Options{})
|
||||||
_, excluded, err := arch.save(ctx, "/", tempfile, nil, false)
|
_, excluded, err := arch.save(ctx, "/", tempfile, nil, false)
|
||||||
@@ -2770,13 +2752,12 @@ func TestDisappearedFile(t *testing.T) {
|
|||||||
back := rtest.Chdir(t, tempdir)
|
back := rtest.Chdir(t, tempdir)
|
||||||
defer back()
|
defer back()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// depending on the underlying FS implementation a missing file may be detected by OpenFile or
|
// depending on the underlying FS implementation a missing file may be detected by OpenFile or
|
||||||
// the subsequent file.Stat() call. Thus test both cases.
|
// the subsequent file.Stat() call. Thus test both cases.
|
||||||
for _, errorOnOpen := range []bool{false, true} {
|
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)
|
_, excluded, err := arch.save(ctx, "/", filepath.Join(tempdir, "testdir"), nil, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
rtest.Assert(t, excluded, "testfile should have been excluded")
|
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) {
|
func statAndSnapshot(t *testing.T, repo archiverRepo, name string) (*data.Node, *data.Node) {
|
||||||
want := nodeFromFile(t, &fs.Local{}, name)
|
want := nodeFromFile(t, fs.NewLocal(), name)
|
||||||
_, node := snapshot(t, repo, &fs.Local{}, nil, name)
|
_, node := snapshot(t, repo, fs.NewLocal(), nil, name)
|
||||||
return want, node
|
return want, node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
func TestBufferPoolReuse(t *testing.T) {
|
func TestBufferPoolReuse(t *testing.T) {
|
||||||
success := false
|
success := false
|
||||||
// retries to avoid flakiness. The test can fail depending on the GC.
|
// 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
|
// Test that buffers are actually reused from the pool
|
||||||
pool := newBufferPool(1024)
|
pool := newBufferPool(1024)
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ func TestBufferPoolReuse(t *testing.T) {
|
|||||||
func TestBufferPoolLargeBuffers(t *testing.T) {
|
func TestBufferPoolLargeBuffers(t *testing.T) {
|
||||||
success := false
|
success := false
|
||||||
// retries to avoid flakiness. The test can fail depending on the GC.
|
// 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
|
// Test that buffers larger than defaultSize are not returned to pool
|
||||||
pool := newBufferPool(1024)
|
pool := newBufferPool(1024)
|
||||||
buf := pool.Get()
|
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 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
|
// non-nil, it is going to be used in the RejectByNameFunc to expedite the evaluation
|
||||||
// of a directory based on previous visits.
|
// 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 == "" {
|
if excludeFileSpec == "" {
|
||||||
return nil, errors.New("name for exclusion tagfile is empty")
|
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
|
// 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
|
// header. If rc is non-nil, it is used to expedite the evaluation of a
|
||||||
// directory based on previous visits.
|
// 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 == "" {
|
if tagFilename == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache,
|
|||||||
return rejected
|
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)
|
tf := fsInst.Join(dir, tagFilename)
|
||||||
_, err := fsInst.Lstat(tf)
|
_, err := fsInst.Lstat(tf)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
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
|
// 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 {
|
return func(item string, fi *fs.ExtendedFileInfo, _ fs.FS) bool {
|
||||||
recall, err := fi.RecallOnDataAccess()
|
recall, err := fi.RecallOnDataAccess()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func TestIsExcludedByFile(t *testing.T) {
|
|||||||
if tc.content == "" {
|
if tc.content == "" {
|
||||||
h = ""
|
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)
|
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -111,8 +111,8 @@ func TestMultipleIsExcludedByFile(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
excludedByFoo := fooExclude(p, nil, &fs.Local{})
|
excludedByFoo := fooExclude(p, nil, fs.NewLocal())
|
||||||
excludedByBar := barExclude(p, nil, &fs.Local{})
|
excludedByBar := barExclude(p, nil, fs.NewLocal())
|
||||||
excluded := excludedByFoo || excludedByBar
|
excluded := excludedByFoo || excludedByBar
|
||||||
// the log message helps debugging in case the test fails
|
// the log message helps debugging in case the test fails
|
||||||
t.Logf("%q: %v || %v = %v", p, excludedByFoo, excludedByBar, excluded)
|
t.Logf("%q: %v || %v = %v", p, excludedByFoo, excludedByBar, excluded)
|
||||||
@@ -243,7 +243,7 @@ func TestDeviceMap(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/restic/chunker"
|
|
||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
@@ -15,36 +14,38 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const chunkReadBufSize = 512 * 1024 // matches chunker internal read buffer size
|
||||||
|
|
||||||
// fileSaver concurrently saves incoming files to the repo.
|
// fileSaver concurrently saves incoming files to the repo.
|
||||||
type fileSaver struct {
|
type fileSaver struct {
|
||||||
saveFilePool *bufferPool
|
saveFilePool *bufferPool
|
||||||
uploader restic.BlobSaverAsync
|
uploader restic.BlobSaverAsync
|
||||||
|
|
||||||
pol chunker.Pol
|
chunkerFactory restic.ChunkerFactory
|
||||||
|
|
||||||
ch chan<- saveFileJob
|
ch chan<- saveFileJob
|
||||||
|
|
||||||
CompleteBlob func(bytes uint64)
|
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
|
// newFileSaver returns a new file saver. A worker pool with fileWorkers is
|
||||||
// started, it is stopped when ctx is cancelled.
|
// 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)
|
ch := make(chan saveFileJob)
|
||||||
debug.Log("new file saver with %v file workers", fileWorkers)
|
debug.Log("new file saver with %v file workers", fileWorkers)
|
||||||
|
|
||||||
s := &fileSaver{
|
s := &fileSaver{
|
||||||
uploader: uploader,
|
uploader: uploader,
|
||||||
saveFilePool: newBufferPool(chunker.MaxSize),
|
saveFilePool: newBufferPool(chunkerFactory.MaxChunkSize()),
|
||||||
pol: pol,
|
chunkerFactory: chunkerFactory,
|
||||||
ch: ch,
|
ch: ch,
|
||||||
|
|
||||||
CompleteBlob: func(uint64) {},
|
CompleteBlob: func(uint64) {},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := uint(0); i < fileWorkers; i++ {
|
for range fileWorkers {
|
||||||
wg.Go(func() error {
|
wg.Go(func() error {
|
||||||
s.worker(ctx, ch)
|
s.worker(ctx, ch)
|
||||||
return nil
|
return nil
|
||||||
@@ -100,8 +101,63 @@ type saveFileJob struct {
|
|||||||
complete fileCompleteFunc
|
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.
|
// 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()
|
start()
|
||||||
|
|
||||||
fnr := futureNodeResult{
|
fnr := futureNodeResult{
|
||||||
@@ -161,15 +217,15 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// reuse the chunker
|
chnker.Reset()
|
||||||
chnker.Reset(f, s.pol)
|
chunkState.reset()
|
||||||
|
|
||||||
node.Content = []restic.ID{}
|
node.Content = []restic.ID{}
|
||||||
node.Size = 0
|
node.Size = 0
|
||||||
var idx int
|
var idx int
|
||||||
for {
|
for {
|
||||||
buf := s.saveFilePool.Get()
|
buf := s.saveFilePool.Get()
|
||||||
chunk, err := chnker.Next(buf.Data)
|
chunkData, err := chunkState.readNextChunk(f, chnker, buf.Data)
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
buf.Release()
|
buf.Release()
|
||||||
break
|
break
|
||||||
@@ -181,8 +237,9 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.Data = chunk.Data
|
// put result buffer back for later reuse
|
||||||
node.Size += uint64(chunk.Length)
|
buf.Data = chunkData
|
||||||
|
node.Size += uint64(len(chunkData))
|
||||||
|
|
||||||
// test if the context has been cancelled, return the error
|
// test if the context has been cancelled, return the error
|
||||||
if ctx.Err() != nil {
|
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{})
|
node.Content = append(node.Content, restic.ID{})
|
||||||
lock.Unlock()
|
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()
|
defer buf.Release()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
completeError(err)
|
completeError(err)
|
||||||
@@ -209,7 +266,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
|||||||
lock.Lock()
|
lock.Lock()
|
||||||
if !known {
|
if !known {
|
||||||
fnr.stats.DataBlobs++
|
fnr.stats.DataBlobs++
|
||||||
fnr.stats.DataSize += uint64(len(buf.Data))
|
fnr.stats.DataSize += uint64(len(chunkData))
|
||||||
fnr.stats.DataSizeInRepo += uint64(sizeInRepo)
|
fnr.stats.DataSizeInRepo += uint64(sizeInRepo)
|
||||||
}
|
}
|
||||||
node.Content[pos] = newID
|
node.Content[pos] = newID
|
||||||
@@ -226,7 +283,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.CompleteBlob(uint64(len(chunk.Data)))
|
s.CompleteBlob(uint64(len(chunkData)))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = f.Close()
|
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) {
|
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 := s.chunkerFactory.NewChunker()
|
||||||
chnker := chunker.New(nil, s.pol)
|
chunkState := &fileChunkState{readBuf: make([]byte, chunkReadBufSize)}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var job saveFileJob
|
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 {
|
if job.completeReading != nil {
|
||||||
job.completeReading()
|
job.completeReading()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/chunker"
|
|
||||||
"github.com/restic/restic/internal/data"
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/test"
|
"github.com/restic/restic/internal/test"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
func createTestFiles(t testing.TB, num int) (files []string) {
|
func createTestFiles(t testing.TB, num int) (files []string) {
|
||||||
tempdir := test.TempDir(t)
|
tempdir := test.TempDir(t)
|
||||||
|
|
||||||
for i := 0; i < num; i++ {
|
for i := range num {
|
||||||
filename := fmt.Sprintf("testfile-%d", i)
|
filename := fmt.Sprintf("testfile-%d", i)
|
||||||
err := os.WriteFile(filepath.Join(tempdir, filename), []byte(filename), 0600)
|
err := os.WriteFile(filepath.Join(tempdir, filename), []byte(filename), 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -34,14 +34,11 @@ func startFileSaver(ctx context.Context, t testing.TB, _ fs.FS) (*fileSaver, *mo
|
|||||||
wg, ctx := errgroup.WithContext(ctx)
|
wg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
workers := uint(runtime.NumCPU())
|
workers := uint(runtime.NumCPU())
|
||||||
pol, err := chunker.RandomPolynomial()
|
chunkerFactory := repository.TestRepository(t).ChunkerFactory()
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
saver := &mockSaver{saved: make(map[string]int)}
|
saver := &mockSaver{saved: make(map[string]int)}
|
||||||
s := newFileSaver(ctx, wg, saver, pol, workers)
|
s := newFileSaver(ctx, wg, saver, chunkerFactory, workers)
|
||||||
s.NodeFromFileInfo = func(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*data.Node, error) {
|
s.NodeFromFileInfo = func(snPath, filename string, meta toNoder, ignoreXattrListError bool) (*data.Node, error) {
|
||||||
return meta.ToNode(ignoreXattrListError, t.Logf)
|
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) {
|
func TestFileSaver(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
startFn := func() {}
|
startFn := func() {}
|
||||||
completeReadingFn := func() {}
|
completeReadingFn := func() {}
|
||||||
completeFn := func(*data.Node, ItemStats) {}
|
completeFn := func(*data.Node, ItemStats) {}
|
||||||
|
|
||||||
files := createTestFiles(t, 15)
|
files := createTestFiles(t, 15)
|
||||||
testFs := fs.Local{}
|
testFs := fs.NewLocal()
|
||||||
s, saver, ctx, wg := startFileSaver(ctx, t, testFs)
|
s, saver, ctx, wg := startFileSaver(ctx, t, testFs)
|
||||||
|
|
||||||
var results []futureNode
|
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 {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir := rtest.TempDir(t)
|
tempdir := rtest.TempDir(t)
|
||||||
TestCreateFiles(t, tempdir, test.src)
|
TestCreateFiles(t, tempdir, test.src)
|
||||||
@@ -92,7 +91,7 @@ func TestScanner(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := NewScanner(fs.Track{FS: fs.Local{}})
|
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
|
||||||
if test.selFn != nil {
|
if test.selFn != nil {
|
||||||
sc.Select = test.selFn
|
sc.Select = test.selFn
|
||||||
}
|
}
|
||||||
@@ -213,8 +212,7 @@ func TestScannerError(t *testing.T) {
|
|||||||
t.Skipf("skip on windows")
|
t.Skipf("skip on windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir := rtest.TempDir(t)
|
tempdir := rtest.TempDir(t)
|
||||||
TestCreateFiles(t, tempdir, test.src)
|
TestCreateFiles(t, tempdir, test.src)
|
||||||
@@ -231,7 +229,7 @@ func TestScannerError(t *testing.T) {
|
|||||||
test.prepare(t)
|
test.prepare(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := NewScanner(fs.Track{FS: fs.Local{}})
|
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
|
||||||
if test.selFn != nil {
|
if test.selFn != nil {
|
||||||
sc.Select = test.selFn
|
sc.Select = test.selFn
|
||||||
}
|
}
|
||||||
@@ -299,7 +297,7 @@ func TestScannerCancel(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := NewScanner(fs.Track{FS: fs.Local{}})
|
sc := NewScanner(fs.Track{FS: fs.NewLocal()})
|
||||||
var lastStats ScanStats
|
var lastStats ScanStats
|
||||||
sc.Result = func(item string, s ScanStats) {
|
sc.Result = func(item string, s ScanStats) {
|
||||||
lastStats = s
|
lastStats = s
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
|
|
||||||
// TestSnapshot creates a new snapshot of path.
|
// TestSnapshot creates a new snapshot of path.
|
||||||
func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *restic.ID) *data.Snapshot {
|
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{
|
opts := SnapshotOptions{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Hostname: "localhost",
|
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.
|
// 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 {
|
func (d TestDir) String() string {
|
||||||
return "<Dir>"
|
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
|
// TestWalkFunc is used by TestWalkFiles to traverse the dir. When an error is
|
||||||
// returned, traversal stops and the surrounding test is marked as failed.
|
// 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
|
// TestWalkFiles runs fn for each file/directory in dir, the filename will be
|
||||||
// constructed with target as the prefix. Symlinks on Windows are ignored.
|
// 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{})
|
pathsChecked := make(map[string]struct{})
|
||||||
|
|
||||||
// first, test that all items are there
|
// 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)
|
fi, err := os.Lstat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -243,7 +243,7 @@ func TestEnsureFileContent(ctx context.Context, t testing.TB, repo restic.BlobLo
|
|||||||
content := make([]byte, len(file.Content))
|
content := make([]byte, len(file.Content))
|
||||||
pos := 0
|
pos := 0
|
||||||
for _, id := range node.Content {
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("error loading blob %v: %v", id.Str(), err)
|
t.Fatalf("error loading blob %v: %v", id.Str(), err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package archiver
|
package archiver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -28,30 +27,30 @@ func (t *MockT) Fail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fatal is equivalent to Log followed by FailNow.
|
// 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.T.Logf("MockT Fatal called with %v", args)
|
||||||
t.HasFailed = true
|
t.HasFailed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatalf is equivalent to Logf followed by FailNow.
|
// 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.T.Logf("MockT Fatal called: "+msg, args...)
|
||||||
t.HasFailed = true
|
t.HasFailed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error is equivalent to Log followed by Fail.
|
// 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.T.Logf("MockT Error called with %v", args)
|
||||||
t.HasFailed = true
|
t.HasFailed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf is equivalent to Logf followed by Fail.
|
// 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.T.Logf("MockT Error called: "+msg, args...)
|
||||||
t.HasFailed = true
|
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 {
|
for name, item := range files {
|
||||||
target := filepath.Join(targetdir, filepath.FromSlash(name))
|
target := filepath.Join(targetdir, filepath.FromSlash(name))
|
||||||
err := os.MkdirAll(filepath.Dir(target), 0700)
|
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) {
|
func TestTestCreateFiles(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
dir TestDir
|
dir TestDir
|
||||||
files map[string]interface{}
|
files map[string]any
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
dir: TestDir{
|
dir: TestDir{
|
||||||
@@ -91,7 +90,7 @@ func TestTestCreateFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
"subdir": TestDir{},
|
"subdir": TestDir{},
|
||||||
"subdir/subfile": TestFile{Content: "bar"},
|
"subdir/subfile": TestFile{Content: "bar"},
|
||||||
@@ -196,7 +195,7 @@ func TestTestWalkFiles(t *testing.T) {
|
|||||||
got := make(map[string]string)
|
got := make(map[string]string)
|
||||||
|
|
||||||
TestCreateFiles(t, tempdir, test.dir)
|
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)
|
p, err := filepath.Rel(tempdir, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -216,11 +215,11 @@ func TestTestWalkFiles(t *testing.T) {
|
|||||||
func TestTestEnsureFiles(t *testing.T) {
|
func TestTestEnsureFiles(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
expectFailure bool
|
expectFailure bool
|
||||||
files map[string]interface{}
|
files map[string]any
|
||||||
want TestDir
|
want TestDir
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
"subdir/subfile": TestFile{Content: "bar"},
|
"subdir/subfile": TestFile{Content: "bar"},
|
||||||
"x/y/link": TestSymlink{Target: filepath.Clean("../../foo")},
|
"x/y/link": TestSymlink{Target: filepath.Clean("../../foo")},
|
||||||
@@ -239,7 +238,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -251,7 +250,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
"subdir/subfile": TestFile{Content: "bar"},
|
"subdir/subfile": TestFile{Content: "bar"},
|
||||||
},
|
},
|
||||||
@@ -261,7 +260,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "xxx"},
|
"foo": TestFile{Content: "xxx"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -270,7 +269,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestSymlink{Target: "/xxx"},
|
"foo": TestSymlink{Target: "/xxx"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -279,7 +278,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -288,7 +287,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestSymlink{Target: "xxx"},
|
"foo": TestSymlink{Target: "xxx"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -297,7 +296,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestDir{
|
"foo": TestDir{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
},
|
},
|
||||||
@@ -308,7 +307,7 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -341,11 +340,11 @@ func TestTestEnsureFiles(t *testing.T) {
|
|||||||
func TestTestEnsureSnapshot(t *testing.T) {
|
func TestTestEnsureSnapshot(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
expectFailure bool
|
expectFailure bool
|
||||||
files map[string]interface{}
|
files map[string]any
|
||||||
want TestDir
|
want TestDir
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
filepath.FromSlash("subdir/subfile"): TestFile{Content: "bar"},
|
filepath.FromSlash("subdir/subfile"): TestFile{Content: "bar"},
|
||||||
filepath.FromSlash("x/y/link"): TestSymlink{Target: filepath.FromSlash("../../foo")},
|
filepath.FromSlash("x/y/link"): TestSymlink{Target: filepath.FromSlash("../../foo")},
|
||||||
@@ -366,7 +365,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -377,7 +376,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
"bar": TestFile{Content: "bar"},
|
"bar": TestFile{Content: "bar"},
|
||||||
},
|
},
|
||||||
@@ -389,7 +388,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -401,7 +400,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -414,7 +413,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
|
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -425,7 +424,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
|
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -436,7 +435,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectFailure: true,
|
expectFailure: true,
|
||||||
files: map[string]interface{}{
|
files: map[string]any{
|
||||||
"foo": TestFile{Content: "foo"},
|
"foo": TestFile{Content: "foo"},
|
||||||
},
|
},
|
||||||
want: TestDir{
|
want: TestDir{
|
||||||
@@ -449,8 +448,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx := t.Context()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tempdir := rtest.TempDir(t)
|
tempdir := rtest.TempDir(t)
|
||||||
|
|
||||||
@@ -467,7 +465,7 @@ func TestTestEnsureSnapshot(t *testing.T) {
|
|||||||
|
|
||||||
repo := repository.TestRepository(t)
|
repo := repository.TestRepository(t)
|
||||||
|
|
||||||
arch := New(repo, fs.Local{}, Options{})
|
arch := New(repo, fs.NewLocal(), Options{})
|
||||||
opts := SnapshotOptions{
|
opts := SnapshotOptions{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Hostname: "localhost",
|
Hostname: "localhost",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user