mirror of
https://github.com/restic/restic.git
synced 2026-10-29 19:45:42 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e72d638df |
No files matched your search
@@ -39,8 +39,8 @@ Please describe the feature you'd like us to add here.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
What are you trying to do? What problem would this solve?
|
What are you trying to do?
|
||||||
---------------------------------------------------------
|
--------------------------
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
This section should contain a brief description what you're trying to do, which
|
This section should contain a brief description what you're trying to do, which
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ your time and add more commits. If you're done and ready for review, please
|
|||||||
check the last box.
|
check the last box.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
What does this PR change? What problem does it solve?
|
What is the purpose of this change? What does it change?
|
||||||
-----------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Describe the changes and their purpose here, as detailed as needed.
|
Describe the changes here, as detailed as needed.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
Was the change discussed in an issue or in the forum before?
|
Was the change discussed in an issue or in the forum before?
|
||||||
@@ -23,8 +23,8 @@ Was the change discussed in an issue or in the forum before?
|
|||||||
<!--
|
<!--
|
||||||
Link issues and relevant forum posts here.
|
Link issues and relevant forum posts here.
|
||||||
|
|
||||||
If this PR resolves an issue on GitHub, use "Closes #1234" so that the issue
|
If this PR resolves an issue on GitHub, use "closes #1234" so that the issue is
|
||||||
is closed automatically when this PR is merged.
|
closed automatically when this PR is merged.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
Checklist
|
Checklist
|
||||||
|
|||||||
+18
-10
@@ -3,6 +3,22 @@ sudo: false
|
|||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
- os: linux
|
||||||
|
go: "1.11.x"
|
||||||
|
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.cache/go-build
|
||||||
|
- $HOME/gopath/pkg/mod
|
||||||
|
|
||||||
|
- os: linux
|
||||||
|
go: "1.12.x"
|
||||||
|
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.cache/go-build
|
||||||
|
- $HOME/gopath/pkg/mod
|
||||||
|
|
||||||
- os: linux
|
- os: linux
|
||||||
go: "1.13.x"
|
go: "1.13.x"
|
||||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||||
@@ -11,17 +27,9 @@ matrix:
|
|||||||
- $HOME/.cache/go-build
|
- $HOME/.cache/go-build
|
||||||
- $HOME/gopath/pkg/mod
|
- $HOME/gopath/pkg/mod
|
||||||
|
|
||||||
- os: linux
|
|
||||||
go: "1.14.x"
|
|
||||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- $HOME/.cache/go-build
|
|
||||||
- $HOME/gopath/pkg/mod
|
|
||||||
|
|
||||||
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
|
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
|
||||||
- os: linux
|
- os: linux
|
||||||
go: "1.15.x"
|
go: "1.14.x"
|
||||||
sudo: true
|
sudo: true
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
@@ -29,7 +37,7 @@ matrix:
|
|||||||
- $HOME/gopath/pkg/mod
|
- $HOME/gopath/pkg/mod
|
||||||
|
|
||||||
- os: osx
|
- os: osx
|
||||||
go: "1.15.x"
|
go: "1.14.x"
|
||||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
|
|||||||
+4
-468
@@ -1,467 +1,3 @@
|
|||||||
Changelog for restic 0.10.0 (2020-09-19)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
The following sections list the changes in restic 0.10.0 relevant to
|
|
||||||
restic users. The changes are ordered by importance.
|
|
||||||
|
|
||||||
Summary
|
|
||||||
-------
|
|
||||||
|
|
||||||
* Fix #1863: Report correct number of directories processed by backup
|
|
||||||
* Fix #2254: Fix tar issues when dumping `/`
|
|
||||||
* Fix #2281: Handle format verbs like '%' properly in `find` output
|
|
||||||
* Fix #2298: Do not hang when run as a background job
|
|
||||||
* Fix #2389: Fix mangled json output of backup command
|
|
||||||
* Fix #2390: Refresh lock timestamp
|
|
||||||
* Fix #2429: Backup --json reports total_bytes_processed as 0
|
|
||||||
* Fix #2469: Fix incorrect bytes stats in `diff` command
|
|
||||||
* Fix #2518: Do not crash with Synology NAS sftp server
|
|
||||||
* Fix #2531: Fix incorrect size calculation in `stats --mode restore-size`
|
|
||||||
* Fix #2537: Fix incorrect file counts in `stats --mode restore-size`
|
|
||||||
* Fix #2592: SFTP backend supports IPv6 addresses
|
|
||||||
* Fix #2607: Honor RESTIC_CACHE_DIR environment variable on Mac and Windows
|
|
||||||
* Fix #2668: Don't abort the stats command when data blobs are missing
|
|
||||||
* Fix #2674: Add stricter prune error checks
|
|
||||||
* Fix #2899: Fix possible crash in the progress bar of check --read-data
|
|
||||||
* Chg #2482: Remove vendored dependencies
|
|
||||||
* Chg #2546: Return exit code 3 when failing to backup all source data
|
|
||||||
* Chg #2600: Update dependencies, require Go >= 1.13
|
|
||||||
* Chg #1597: Honor the --no-lock flag in the mount command
|
|
||||||
* Enh #1570: Support specifying multiple host flags for various commands
|
|
||||||
* Enh #1680: Optimize `restic mount`
|
|
||||||
* Enh #2072: Display snapshot date when using `restic find`
|
|
||||||
* Enh #2175: Allow specifying user and host when creating keys
|
|
||||||
* Enh #2277: Add support for ppc64le
|
|
||||||
* Enh #2395: Ignore sync errors when operation not supported by local filesystem
|
|
||||||
* Enh #2427: Add flag `--iexclude-file` to backup command
|
|
||||||
* Enh #2569: Support excluding files by their size
|
|
||||||
* Enh #2571: Self-heal missing file parts during backup of unchanged files
|
|
||||||
* Enh #2858: Support filtering snapshots by tag and path in the stats command
|
|
||||||
* Enh #323: Add command for copying snapshots between repositories
|
|
||||||
* Enh #551: Use optimized library for hash calculation of file chunks
|
|
||||||
* Enh #2195: Simplify and improve restore performance
|
|
||||||
* Enh #2328: Improve speed of check command
|
|
||||||
* Enh #2423: Support user@domain parsing as user
|
|
||||||
* Enh #2576: Improve the chunking algorithm
|
|
||||||
* Enh #2598: Improve speed of diff command
|
|
||||||
* Enh #2599: Slightly reduce memory usage of prune and stats commands
|
|
||||||
* Enh #2733: S3 backend: Add support for WebIdentityTokenFile
|
|
||||||
* Enh #2773: Optimize handling of new index entries
|
|
||||||
* Enh #2781: Reduce memory consumption of in-memory index
|
|
||||||
* Enh #2786: Optimize `list blobs` command
|
|
||||||
* Enh #2790: Optimized file access in restic mount
|
|
||||||
* Enh #2840: Speed-up file deletion in forget, prune and rebuild-index
|
|
||||||
|
|
||||||
Details
|
|
||||||
-------
|
|
||||||
|
|
||||||
* Bugfix #1863: Report correct number of directories processed by backup
|
|
||||||
|
|
||||||
The directory statistics calculation was fixed to report the actual number of processed
|
|
||||||
directories instead of always zero.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/1863
|
|
||||||
|
|
||||||
* Bugfix #2254: Fix tar issues when dumping `/`
|
|
||||||
|
|
||||||
We've fixed an issue with dumping either `/` or files on the first sublevel e.g. `/foo` to tar.
|
|
||||||
This also fixes tar dumping issues on Windows where this issue could also happen.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2254
|
|
||||||
https://github.com/restic/restic/issues/2357
|
|
||||||
https://github.com/restic/restic/pull/2255
|
|
||||||
|
|
||||||
* Bugfix #2281: Handle format verbs like '%' properly in `find` output
|
|
||||||
|
|
||||||
The JSON or "normal" output of the `find` command can now deal with file names that contain
|
|
||||||
substrings which the Golang `fmt` package considers "format verbs" like `%s`.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2281
|
|
||||||
|
|
||||||
* Bugfix #2298: Do not hang when run as a background job
|
|
||||||
|
|
||||||
Restic did hang on exit while restoring the terminal configuration when it was started as a
|
|
||||||
background job, for example using `restic ... &`. This has been fixed by only restoring the
|
|
||||||
terminal configuration when restic is interrupted while reading a password from the
|
|
||||||
terminal.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2298
|
|
||||||
|
|
||||||
* Bugfix #2389: Fix mangled json output of backup command
|
|
||||||
|
|
||||||
We've fixed a race condition in the json output of the backup command that could cause multiple
|
|
||||||
lines to get mixed up. We've also ensured that the backup summary is printed last.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2389
|
|
||||||
https://github.com/restic/restic/pull/2545
|
|
||||||
|
|
||||||
* Bugfix #2390: Refresh lock timestamp
|
|
||||||
|
|
||||||
Long-running operations did not refresh lock timestamp, resulting in locks becoming stale.
|
|
||||||
This is now fixed.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2390
|
|
||||||
|
|
||||||
* Bugfix #2429: Backup --json reports total_bytes_processed as 0
|
|
||||||
|
|
||||||
We've fixed the json output of total_bytes_processed. The non-json output was already fixed
|
|
||||||
with pull request #2138 but left the json output untouched.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2429
|
|
||||||
|
|
||||||
* Bugfix #2469: Fix incorrect bytes stats in `diff` command
|
|
||||||
|
|
||||||
In some cases, the wrong number of bytes (e.g. 16777215.998 TiB) were reported by the `diff`
|
|
||||||
command. This is now fixed.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2469
|
|
||||||
|
|
||||||
* Bugfix #2518: Do not crash with Synology NAS sftp server
|
|
||||||
|
|
||||||
It was found that when restic is used to store data on an sftp server on a Synology NAS with a
|
|
||||||
relative path (one which does not start with a slash), it may go into an endless loop trying to
|
|
||||||
create directories on the server. We've fixed this bug by using a function in the sftp library
|
|
||||||
instead of our own implementation.
|
|
||||||
|
|
||||||
The bug was discovered because the Synology sftp server behaves erratic with non-absolute
|
|
||||||
path (e.g. `home/restic-repo`). This can be resolved by just using an absolute path instead
|
|
||||||
(`/home/restic-repo`). We've also added a paragraph in the FAQ.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2518
|
|
||||||
https://github.com/restic/restic/issues/2363
|
|
||||||
https://github.com/restic/restic/pull/2530
|
|
||||||
|
|
||||||
* Bugfix #2531: Fix incorrect size calculation in `stats --mode restore-size`
|
|
||||||
|
|
||||||
The restore-size mode of stats was counting hard-linked files as if they were independent.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2531
|
|
||||||
|
|
||||||
* Bugfix #2537: Fix incorrect file counts in `stats --mode restore-size`
|
|
||||||
|
|
||||||
The restore-size mode of stats was failing to count empty directories and some files with hard
|
|
||||||
links.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2537
|
|
||||||
|
|
||||||
* Bugfix #2592: SFTP backend supports IPv6 addresses
|
|
||||||
|
|
||||||
The SFTP backend now supports IPv6 addresses natively, without relying on aliases in the
|
|
||||||
external SSH configuration.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2592
|
|
||||||
|
|
||||||
* Bugfix #2607: Honor RESTIC_CACHE_DIR environment variable on Mac and Windows
|
|
||||||
|
|
||||||
On Mac and Windows, the RESTIC_CACHE_DIR environment variable was ignored. This variable can
|
|
||||||
now be used on all platforms to set the directory where restic stores caches.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2607
|
|
||||||
|
|
||||||
* Bugfix #2668: Don't abort the stats command when data blobs are missing
|
|
||||||
|
|
||||||
Runing the stats command in the blobs-per-file mode on a repository with missing data blobs
|
|
||||||
previously resulted in a crash.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2668
|
|
||||||
|
|
||||||
* Bugfix #2674: Add stricter prune error checks
|
|
||||||
|
|
||||||
Additional checks were added to the prune command in order to improve resiliency to backend,
|
|
||||||
hardware and/or networking issues. The checks now detect a few more cases where such outside
|
|
||||||
factors could potentially cause data loss.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2674
|
|
||||||
|
|
||||||
* Bugfix #2899: Fix possible crash in the progress bar of check --read-data
|
|
||||||
|
|
||||||
We've fixed a possible crash while displaying the progress bar for the check --read-data
|
|
||||||
command. The crash occurred when the length of the progress bar status exceeded the terminal
|
|
||||||
width, which only happened for very narrow terminal windows.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2899
|
|
||||||
https://forum.restic.net/t/restic-rclone-pcloud-connection-issues/2963/15
|
|
||||||
|
|
||||||
* Change #2482: Remove vendored dependencies
|
|
||||||
|
|
||||||
We've removed the vendored dependencies (in the subdir `vendor/`). When building restic, the
|
|
||||||
Go compiler automatically fetches the dependencies. It will also cryptographically verify
|
|
||||||
that the correct code has been fetched by using the hashes in `go.sum` (see the link to the
|
|
||||||
documentation below).
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2482
|
|
||||||
https://golang.org/cmd/go/#hdr-Module_downloading_and_verification
|
|
||||||
|
|
||||||
* Change #2546: Return exit code 3 when failing to backup all source data
|
|
||||||
|
|
||||||
The backup command used to return a zero exit code as long as a snapshot could be created
|
|
||||||
successfully, even if some of the source files could not be read (in which case the snapshot
|
|
||||||
would contain the rest of the files).
|
|
||||||
|
|
||||||
This made it hard for automation/scripts to detect failures/incomplete backups by looking at
|
|
||||||
the exit code. Restic now returns the following exit codes for the backup command:
|
|
||||||
|
|
||||||
- 0 when the command was successful - 1 when there was a fatal error (no snapshot created) - 3 when
|
|
||||||
some source data could not be read (incomplete snapshot created)
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/956
|
|
||||||
https://github.com/restic/restic/issues/2064
|
|
||||||
https://github.com/restic/restic/issues/2526
|
|
||||||
https://github.com/restic/restic/issues/2364
|
|
||||||
https://github.com/restic/restic/pull/2546
|
|
||||||
|
|
||||||
* Change #2600: Update dependencies, require Go >= 1.13
|
|
||||||
|
|
||||||
Restic now requires Go to be at least 1.13. This allows simplifications in the build process and
|
|
||||||
removing workarounds.
|
|
||||||
|
|
||||||
This is also probably the last version of restic still supporting mounting repositories via
|
|
||||||
fuse on macOS. The library we're using for fuse does not support macOS any more and osxfuse is not
|
|
||||||
open source any more.
|
|
||||||
|
|
||||||
https://github.com/bazil/fuse/issues/224
|
|
||||||
https://github.com/osxfuse/osxfuse/issues/590
|
|
||||||
https://github.com/restic/restic/pull/2600
|
|
||||||
https://github.com/restic/restic/pull/2852
|
|
||||||
https://github.com/restic/restic/pull/2927
|
|
||||||
|
|
||||||
* Change #1597: Honor the --no-lock flag in the mount command
|
|
||||||
|
|
||||||
The mount command now does not lock the repository if given the --no-lock flag. This allows to
|
|
||||||
mount repositories which are archived on a read only backend/filesystem.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/1597
|
|
||||||
https://github.com/restic/restic/pull/2821
|
|
||||||
|
|
||||||
* Enhancement #1570: Support specifying multiple host flags for various commands
|
|
||||||
|
|
||||||
Previously commands didn't take more than one `--host` or `-H` argument into account, which
|
|
||||||
could be limiting with e.g. the `forget` command.
|
|
||||||
|
|
||||||
The `dump`, `find`, `forget`, `ls`, `mount`, `restore`, `snapshots`, `stats` and `tag`
|
|
||||||
commands will now take into account multiple `--host` and `-H` flags.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/1570
|
|
||||||
|
|
||||||
* Enhancement #1680: Optimize `restic mount`
|
|
||||||
|
|
||||||
We've optimized the FUSE implementation used within restic. `restic mount` is now more
|
|
||||||
responsive and uses less memory.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/1680
|
|
||||||
https://github.com/restic/restic/pull/2587
|
|
||||||
https://github.com/restic/restic/pull/2787
|
|
||||||
|
|
||||||
* Enhancement #2072: Display snapshot date when using `restic find`
|
|
||||||
|
|
||||||
Added the respective snapshot date to the output of `restic find`.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2072
|
|
||||||
|
|
||||||
* Enhancement #2175: Allow specifying user and host when creating keys
|
|
||||||
|
|
||||||
When adding a new key to the repository, the username and hostname for the new key can be
|
|
||||||
specified on the command line. This allows overriding the defaults, for example if you would
|
|
||||||
prefer to use the FQDN to identify the host or if you want to add keys for several different hosts
|
|
||||||
without having to run the key add command on those hosts.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2175
|
|
||||||
|
|
||||||
* Enhancement #2277: Add support for ppc64le
|
|
||||||
|
|
||||||
Adds support for ppc64le, the processor architecture from IBM.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2277
|
|
||||||
|
|
||||||
* Enhancement #2395: Ignore sync errors when operation not supported by local filesystem
|
|
||||||
|
|
||||||
The local backend has been modified to work with filesystems which doesn't support the `sync`
|
|
||||||
operation. This operation is normally used by restic to ensure that data files are fully
|
|
||||||
written to disk before continuing.
|
|
||||||
|
|
||||||
For these limited filesystems, saving a file in the backend would previously fail with an
|
|
||||||
"operation not supported" error. This error is now ignored, which means that e.g. an SMB mount
|
|
||||||
on macOS can now be used as storage location for a repository.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2395
|
|
||||||
https://forum.restic.net/t/sync-errors-on-mac-over-smb/1859
|
|
||||||
|
|
||||||
* Enhancement #2427: Add flag `--iexclude-file` to backup command
|
|
||||||
|
|
||||||
The backup command now supports the flag `--iexclude-file` which is a case-insensitive
|
|
||||||
version of `--exclude-file`.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2427
|
|
||||||
https://github.com/restic/restic/pull/2898
|
|
||||||
|
|
||||||
* Enhancement #2569: Support excluding files by their size
|
|
||||||
|
|
||||||
The `backup` command now supports the `--exclude-larger-than` option to exclude files which
|
|
||||||
are larger than the specified maximum size. This can for example be useful to exclude
|
|
||||||
unimportant files with a large file size.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2569
|
|
||||||
https://github.com/restic/restic/pull/2914
|
|
||||||
|
|
||||||
* Enhancement #2571: Self-heal missing file parts during backup of unchanged files
|
|
||||||
|
|
||||||
We've improved the resilience of restic to certain types of repository corruption.
|
|
||||||
|
|
||||||
For files that are unchanged since the parent snapshot, the backup command now verifies that
|
|
||||||
all parts of the files still exist in the repository. Parts that are missing, e.g. from a damaged
|
|
||||||
repository, are backed up again. This verification was already run for files that were
|
|
||||||
modified since the parent snapshot, but is now also done for unchanged files.
|
|
||||||
|
|
||||||
Note that restic will not backup file parts that are referenced in the index but where the actual
|
|
||||||
data is not present on disk, as this situation can only be detected by restic check. Please
|
|
||||||
ensure that you run `restic check` regularly.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2571
|
|
||||||
https://github.com/restic/restic/pull/2827
|
|
||||||
|
|
||||||
* Enhancement #2858: Support filtering snapshots by tag and path in the stats command
|
|
||||||
|
|
||||||
We've added filtering snapshots by `--tag tagList` and by `--path path` to the `stats`
|
|
||||||
command. This includes filtering of only 'latest' snapshots or all snapshots in a repository.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2858
|
|
||||||
https://github.com/restic/restic/pull/2859
|
|
||||||
https://forum.restic.net/t/stats-for-a-host-and-filtered-snapshots/3020
|
|
||||||
|
|
||||||
* Enhancement #323: Add command for copying snapshots between repositories
|
|
||||||
|
|
||||||
We've added a copy command, allowing you to copy snapshots from one repository to another.
|
|
||||||
|
|
||||||
Note that this process will have to read (download) and write (upload) the entire snapshot(s)
|
|
||||||
due to the different encryption keys used on the source and destination repository. Also, the
|
|
||||||
transferred files are not re-chunked, which may break deduplication between files already
|
|
||||||
stored in the destination repo and files copied there using this command.
|
|
||||||
|
|
||||||
To fully support deduplication between repositories when the copy command is used, the init
|
|
||||||
command now supports the `--copy-chunker-params` option, which initializes the new
|
|
||||||
repository with identical parameters for splitting files into chunks as an already existing
|
|
||||||
repository. This allows copied snapshots to be equally deduplicated in both repositories.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/323
|
|
||||||
https://github.com/restic/restic/pull/2606
|
|
||||||
https://github.com/restic/restic/pull/2928
|
|
||||||
|
|
||||||
* Enhancement #551: Use optimized library for hash calculation of file chunks
|
|
||||||
|
|
||||||
We've switched the library used to calculate the hashes of file chunks, which are used for
|
|
||||||
deduplication, to the optimized Minio SHA-256 implementation.
|
|
||||||
|
|
||||||
Depending on the CPU it improves the hashing throughput by 10-30%. Modern x86 CPUs with the SHA
|
|
||||||
Extension should be about two to three times faster.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/551
|
|
||||||
https://github.com/restic/restic/pull/2709
|
|
||||||
|
|
||||||
* Enhancement #2195: Simplify and improve restore performance
|
|
||||||
|
|
||||||
Significantly improves restore performance of large files (i.e. 50M+):
|
|
||||||
https://github.com/restic/restic/issues/2074
|
|
||||||
https://forum.restic.net/t/restore-using-rclone-gdrive-backend-is-slow/1112/8
|
|
||||||
https://forum.restic.net/t/degraded-restore-performance-s3-backend/1400
|
|
||||||
|
|
||||||
Fixes "not enough cache capacity" error during restore:
|
|
||||||
https://github.com/restic/restic/issues/2244
|
|
||||||
|
|
||||||
NOTE: This new implementation does not guarantee order in which blobs are written to the target
|
|
||||||
files and, for example, the last blob of a file can be written to the file before any of the
|
|
||||||
preceeding file blobs. It is therefore possible to have gaps in the data written to the target
|
|
||||||
files if restore fails or interrupted by the user.
|
|
||||||
|
|
||||||
The implementation will try to preallocate space for the restored files on the filesystem to
|
|
||||||
prevent file fragmentation. This ensures good read performance for large files, like for
|
|
||||||
example VM images. If preallocating space is not supported by the filesystem, then this step is
|
|
||||||
silently skipped.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2195
|
|
||||||
https://github.com/restic/restic/pull/2893
|
|
||||||
|
|
||||||
* Enhancement #2328: Improve speed of check command
|
|
||||||
|
|
||||||
We've improved the check command to traverse trees only once independent of whether they are
|
|
||||||
contained in multiple snapshots. The check command is now much faster for repositories with a
|
|
||||||
large number of snapshots.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2284
|
|
||||||
https://github.com/restic/restic/pull/2328
|
|
||||||
|
|
||||||
* Enhancement #2423: Support user@domain parsing as user
|
|
||||||
|
|
||||||
Added the ability for user@domain-like users to be authenticated over SFTP servers.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2423
|
|
||||||
|
|
||||||
* Enhancement #2576: Improve the chunking algorithm
|
|
||||||
|
|
||||||
We've updated the chunker library responsible for splitting files into smaller blocks. It
|
|
||||||
should improve the chunking throughput by 5-15% depending on the CPU.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2820
|
|
||||||
https://github.com/restic/restic/pull/2576
|
|
||||||
https://github.com/restic/restic/pull/2845
|
|
||||||
|
|
||||||
* Enhancement #2598: Improve speed of diff command
|
|
||||||
|
|
||||||
We've improved the performance of the diff command when comparing snapshots with similar
|
|
||||||
content. It should run up to twice as fast as before.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2598
|
|
||||||
|
|
||||||
* Enhancement #2599: Slightly reduce memory usage of prune and stats commands
|
|
||||||
|
|
||||||
The prune and the stats command kept directory identifiers in memory twice while searching for
|
|
||||||
used blobs.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2599
|
|
||||||
|
|
||||||
* Enhancement #2733: S3 backend: Add support for WebIdentityTokenFile
|
|
||||||
|
|
||||||
We've added support for EKS IAM roles for service accounts feature to the S3 backend.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2703
|
|
||||||
https://github.com/restic/restic/pull/2733
|
|
||||||
|
|
||||||
* Enhancement #2773: Optimize handling of new index entries
|
|
||||||
|
|
||||||
Restic now uses less memory for backups which add a lot of data, e.g. large initial backups. In
|
|
||||||
addition, we've improved the stability in some edge cases.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2773
|
|
||||||
|
|
||||||
* Enhancement #2781: Reduce memory consumption of in-memory index
|
|
||||||
|
|
||||||
We've improved how the index is stored in memory. This change can reduce memory usage for large
|
|
||||||
repositories by up to 50% (depending on the operation).
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2781
|
|
||||||
https://github.com/restic/restic/pull/2812
|
|
||||||
|
|
||||||
* Enhancement #2786: Optimize `list blobs` command
|
|
||||||
|
|
||||||
We've changed the implementation of `list blobs` which should be now a bit faster and consume
|
|
||||||
almost no memory even for large repositories.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2786
|
|
||||||
|
|
||||||
* Enhancement #2790: Optimized file access in restic mount
|
|
||||||
|
|
||||||
Reading large (> 100GiB) files from restic mountpoints is now faster, and the speedup is
|
|
||||||
greater for larger files.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2790
|
|
||||||
|
|
||||||
* Enhancement #2840: Speed-up file deletion in forget, prune and rebuild-index
|
|
||||||
|
|
||||||
We've sped up the file deletion for the commands forget, prune and rebuild-index, especially
|
|
||||||
for remote repositories. Deletion was sequential before and is now run in parallel.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2840
|
|
||||||
|
|
||||||
|
|
||||||
Changelog for restic 0.9.6 (2019-11-22)
|
Changelog for restic 0.9.6 (2019-11-22)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
@@ -1825,10 +1361,10 @@ Details
|
|||||||
|
|
||||||
Exploiting the vulnerability requires a Linux/Unix system which saves backups via restic and
|
Exploiting the vulnerability requires a Linux/Unix system which saves backups via restic and
|
||||||
a Windows systems which restores files from the repo. In addition, the attackers need to be able
|
a Windows systems which restores files from the repo. In addition, the attackers need to be able
|
||||||
to create files with arbitrary names which are then saved to the restic repo. For example, by
|
to create create files with arbitrary names which are then saved to the restic repo. For
|
||||||
creating a file named "..\test.txt" (which is a perfectly legal filename on Linux) and
|
example, by creating a file named "..\test.txt" (which is a perfectly legal filename on Linux)
|
||||||
restoring a snapshot containing this file on Windows, it would be written to the parent of the
|
and restoring a snapshot containing this file on Windows, it would be written to the parent of
|
||||||
target directory.
|
the target directory.
|
||||||
|
|
||||||
We'd like to thank Tyler Spivey for reporting this responsibly!
|
We'd like to thank Tyler Spivey for reporting this responsibly!
|
||||||
|
|
||||||
|
|||||||
+54
-19
@@ -60,11 +60,16 @@ uploading it somewhere or post only the parts that are really relevant.
|
|||||||
Development Environment
|
Development Environment
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
The repository contains the code written for restic in the directories
|
The repository contains several sets of directories with code: `cmd/` and
|
||||||
`cmd/` and `internal/`.
|
`internal/` contain the code written for restic, whereas `vendor/` contains
|
||||||
|
copies of libraries restic depends on. The libraries are managed with the
|
||||||
|
command `go mod vendor`.
|
||||||
|
|
||||||
Restic requires Go version 1.12 or later for compiling. Clone the repo (without
|
Go >= 1.11
|
||||||
having `$GOPATH` set) and `cd` into the directory:
|
----------
|
||||||
|
|
||||||
|
For Go version 1.11 or later, you should clone the repo (without having
|
||||||
|
`$GOPATH` set) and `cd` into the directory:
|
||||||
|
|
||||||
$ unset GOPATH
|
$ unset GOPATH
|
||||||
$ git clone https://github.com/restic/restic
|
$ git clone https://github.com/restic/restic
|
||||||
@@ -74,12 +79,40 @@ Then use the `go` tool to build restic:
|
|||||||
|
|
||||||
$ go build ./cmd/restic
|
$ go build ./cmd/restic
|
||||||
$ ./restic version
|
$ ./restic version
|
||||||
restic 0.9.6-dev (compiled manually) compiled with go1.14 on linux/amd64
|
restic 0.9.2-dev (compiled manually) compiled with go1.11 on linux/amd64
|
||||||
|
|
||||||
You can run all tests with the following command:
|
You can run all tests with the following command:
|
||||||
|
|
||||||
$ go test ./...
|
$ go test ./...
|
||||||
|
|
||||||
|
Go < 1.11
|
||||||
|
---------
|
||||||
|
|
||||||
|
In order to compile restic with Go before 1.11, it needs to be checked out at
|
||||||
|
the right path within a `GOPATH`. The concept of a `GOPATH` is explained in
|
||||||
|
["How to write Go code"](https://golang.org/doc/code.html).
|
||||||
|
|
||||||
|
If you do not have a directory with Go code yet, executing the following
|
||||||
|
instructions in your shell will create one for you and check out the restic
|
||||||
|
repo:
|
||||||
|
|
||||||
|
$ export GOPATH="$HOME/go"
|
||||||
|
$ mkdir -p "$GOPATH/src/github.com/restic"
|
||||||
|
$ cd "$GOPATH/src/github.com/restic"
|
||||||
|
$ git clone https://github.com/restic/restic
|
||||||
|
$ cd restic
|
||||||
|
|
||||||
|
You can then build restic as follows:
|
||||||
|
|
||||||
|
$ go build ./cmd/restic
|
||||||
|
$ ./restic version
|
||||||
|
restic compiled manually
|
||||||
|
compiled with go1.8.3 on linux/amd64
|
||||||
|
|
||||||
|
The following commands can be used to run all the tests:
|
||||||
|
|
||||||
|
$ go test ./...
|
||||||
|
|
||||||
Providing Patches
|
Providing Patches
|
||||||
=================
|
=================
|
||||||
|
|
||||||
@@ -92,14 +125,15 @@ down to the following steps:
|
|||||||
GitHub. For a new feature, please add an issue before starting to work on
|
GitHub. For a new feature, please add an issue before starting to work on
|
||||||
it, so that duplicate work is prevented.
|
it, so that duplicate work is prevented.
|
||||||
|
|
||||||
1. Next, fork our project on GitHub if you haven't done so already.
|
1. First we would kindly ask you to fork our project on GitHub if you haven't
|
||||||
|
done so already.
|
||||||
|
|
||||||
2. Clone your fork of the repository locally and **create a new branch** for
|
2. Clone the repository locally and create a new branch. If you are working on
|
||||||
your changes. If you are working on the code itself, please set up the
|
the code itself, please set up the development environment as described in
|
||||||
development environment as described in the previous section.
|
the previous section.
|
||||||
|
|
||||||
3. Commit your changes to the new branch as fine grained as possible, as
|
3. Then commit your changes as fine grained as possible, as smaller patches,
|
||||||
smaller patches, for individual changes, are easier to discuss and merge.
|
that handle one and only one issue are easier to discuss and merge.
|
||||||
|
|
||||||
4. Push the new branch with your changes to your fork of the repository.
|
4. Push the new branch with your changes to your fork of the repository.
|
||||||
|
|
||||||
@@ -112,19 +146,20 @@ down to the following steps:
|
|||||||
existing commit, use common sense to decide which is better), they will be
|
existing commit, use common sense to decide which is better), they will be
|
||||||
automatically added to the pull request.
|
automatically added to the pull request.
|
||||||
|
|
||||||
7. If your pull request changes anything that users should be aware of
|
7. If your pull request changes anything that users should be aware
|
||||||
(a bugfix, a new feature, ...) please add an entry as a new file in
|
of (a bugfix, a new feature, ...) please add an entry as a new
|
||||||
`changelog/unreleased` including the issue number in the filename (e.g.
|
file in `changelog/unreleased` including the issue number in the
|
||||||
`issue-8756`). Use the template in `changelog/TEMPLATE` for the content.
|
filename (e.g. `issue-8756`). Use the template in
|
||||||
It will be used in the announcement of the next stable release. While
|
`changelog/TEMPLATE` for the content. It will be used in the
|
||||||
writing, ask yourself: If I were the user, what would I need to be aware
|
announcement of the next stable release. While writing, ask
|
||||||
of with this change?
|
yourself: If I were the user, what would I need to be aware of
|
||||||
|
with this change.
|
||||||
|
|
||||||
8. Once your code looks good and passes all the tests, we'll merge it. Thanks
|
8. Once your code looks good and passes all the tests, we'll merge it. Thanks
|
||||||
a lot for your contribution!
|
a lot for your contribution!
|
||||||
|
|
||||||
Please provide the patches for each bug or feature in a separate branch and
|
Please provide the patches for each bug or feature in a separate branch and
|
||||||
open up a pull request for each, as this simplifies discussion and merging.
|
open up a pull request for each.
|
||||||
|
|
||||||
The restic project uses the `gofmt` tool for Go source indentation, so please
|
The restic project uses the `gofmt` tool for Go source indentation, so please
|
||||||
run
|
run
|
||||||
|
|||||||
+2
-2
@@ -20,8 +20,8 @@ init:
|
|||||||
|
|
||||||
install:
|
install:
|
||||||
- rmdir c:\go /s /q
|
- rmdir c:\go /s /q
|
||||||
- appveyor DownloadFile https://dl.google.com/go/go1.15.2.windows-amd64.msi
|
- appveyor DownloadFile https://dl.google.com/go/go1.14.windows-amd64.msi
|
||||||
- msiexec /i go1.15.2.windows-amd64.msi /q
|
- msiexec /i go1.14.windows-amd64.msi /q
|
||||||
- go version
|
- go version
|
||||||
- go env
|
- go env
|
||||||
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip
|
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// This program aims to make building Go programs for end users easier by just
|
// This program aims to make building Go programs for end users easier by just
|
||||||
// calling it with `go run`, without having to setup a GOPATH.
|
// calling it with `go run`, without having to setup a GOPATH.
|
||||||
//
|
//
|
||||||
// This program needs Go >= 1.12. It'll use Go modules for compilation. It
|
// This program needs Go >= 1.11. It'll use Go modules for compilation. It
|
||||||
// builds the package configured as Main in the Config struct.
|
// builds the package configured as Main in the Config struct.
|
||||||
|
|
||||||
// BSD 2-Clause License
|
// BSD 2-Clause License
|
||||||
@@ -327,8 +327,8 @@ func (v GoVersion) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if !goVersion.AtLeast(GoVersion{1, 12, 0}) {
|
if !goVersion.AtLeast(GoVersion{1, 11, 0}) {
|
||||||
die("Go version (%v) is too old, restic requires Go >= 1.12\n", goVersion)
|
die("Go version (%v) is too old, Go <= 1.11 does not support Go Modules\n", goVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !goVersion.AtLeast(config.MinVersion) {
|
if !goVersion.AtLeast(config.MinVersion) {
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
Enhancement: Optimize `restic mount`
|
|
||||||
|
|
||||||
We've optimized the FUSE implementation used within restic.
|
|
||||||
`restic mount` is now more responsive and uses less memory.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/1680
|
|
||||||
https://github.com/restic/restic/pull/2587
|
|
||||||
https://github.com/restic/restic/pull/2787
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Bugfix: Report correct number of directories processed by backup
|
|
||||||
|
|
||||||
The directory statistics calculation was fixed to report the actual number
|
|
||||||
of processed directories instead of always zero.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/1863
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Enhancement: Allow specifying user and host when creating keys
|
|
||||||
|
|
||||||
When adding a new key to the repository, the username and hostname for the new
|
|
||||||
key can be specified on the command line. This allows overriding the defaults,
|
|
||||||
for example if you would prefer to use the FQDN to identify the host or if you
|
|
||||||
want to add keys for several different hosts without having to run the key add
|
|
||||||
command on those hosts.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2175
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Bugfix: Fix tar issues when dumping `/`
|
|
||||||
|
|
||||||
We've fixed an issue with dumping either `/` or files on the first sublevel
|
|
||||||
e.g. `/foo` to tar. This also fixes tar dumping issues on Windows where this
|
|
||||||
issue could also happen.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2254
|
|
||||||
https://github.com/restic/restic/issues/2357
|
|
||||||
https://github.com/restic/restic/pull/2255
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
Enhancement: Ignore sync errors when operation not supported by local filesystem
|
|
||||||
|
|
||||||
The local backend has been modified to work with filesystems which doesn't support
|
|
||||||
the `sync` operation. This operation is normally used by restic to ensure that data
|
|
||||||
files are fully written to disk before continuing.
|
|
||||||
|
|
||||||
For these limited filesystems, saving a file in the backend would previously fail with
|
|
||||||
an "operation not supported" error. This error is now ignored, which means that e.g.
|
|
||||||
an SMB mount on macOS can now be used as storage location for a repository.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2395
|
|
||||||
https://forum.restic.net/t/sync-errors-on-mac-over-smb/1859
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Enhancement: Add flag `--iexclude-file` to backup command
|
|
||||||
|
|
||||||
The backup command now supports the flag `--iexclude-file` which is a
|
|
||||||
case-insensitive version of `--exclude-file`.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2427
|
|
||||||
https://github.com/restic/restic/pull/2898
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Enhancement: Support excluding files by their size
|
|
||||||
|
|
||||||
The `backup` command now supports the `--exclude-larger-than` option to exclude files which are
|
|
||||||
larger than the specified maximum size. This can for example be useful to exclude unimportant
|
|
||||||
files with a large file size.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2569
|
|
||||||
https://github.com/restic/restic/pull/2914
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
Enhancement: Self-heal missing file parts during backup of unchanged files
|
|
||||||
|
|
||||||
We've improved the resilience of restic to certain types of repository corruption.
|
|
||||||
|
|
||||||
For files that are unchanged since the parent snapshot, the backup command now
|
|
||||||
verifies that all parts of the files still exist in the repository. Parts that are
|
|
||||||
missing, e.g. from a damaged repository, are backed up again. This verification
|
|
||||||
was already run for files that were modified since the parent snapshot, but is
|
|
||||||
now also done for unchanged files.
|
|
||||||
|
|
||||||
Note that restic will not backup file parts that are referenced in the index but
|
|
||||||
where the actual data is not present on disk, as this situation can only be
|
|
||||||
detected by restic check. Please ensure that you run `restic check` regularly.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2571
|
|
||||||
https://github.com/restic/restic/pull/2827
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Enhancement: Support filtering snapshots by tag and path in the stats command
|
|
||||||
|
|
||||||
We've added filtering snapshots by `--tag tagList` and by `--path path` to
|
|
||||||
the `stats` command. This includes filtering of only 'latest' snapshots or
|
|
||||||
all snapshots in a repository.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2858
|
|
||||||
https://github.com/restic/restic/pull/2859
|
|
||||||
https://forum.restic.net/t/stats-for-a-host-and-filtered-snapshots/3020
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
Enhancement: Add command for copying snapshots between repositories
|
|
||||||
|
|
||||||
We've added a copy command, allowing you to copy snapshots from one
|
|
||||||
repository to another.
|
|
||||||
|
|
||||||
Note that this process will have to read (download) and write (upload) the
|
|
||||||
entire snapshot(s) due to the different encryption keys used on the source
|
|
||||||
and destination repository. Also, the transferred files are not re-chunked,
|
|
||||||
which may break deduplication between files already stored in the
|
|
||||||
destination repo and files copied there using this command.
|
|
||||||
|
|
||||||
To fully support deduplication between repositories when the copy command is
|
|
||||||
used, the init command now supports the `--copy-chunker-params` option,
|
|
||||||
which initializes the new repository with identical parameters for splitting
|
|
||||||
files into chunks as an already existing repository. This allows copied
|
|
||||||
snapshots to be equally deduplicated in both repositories.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/323
|
|
||||||
https://github.com/restic/restic/pull/2606
|
|
||||||
https://github.com/restic/restic/pull/2928
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
Enhancement: Use optimized library for hash calculation of file chunks
|
|
||||||
|
|
||||||
We've switched the library used to calculate the hashes of file chunks, which
|
|
||||||
are used for deduplication, to the optimized Minio SHA-256 implementation.
|
|
||||||
|
|
||||||
Depending on the CPU it improves the hashing throughput by 10-30%. Modern x86
|
|
||||||
CPUs with the SHA Extension should be about two to three times faster.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/551
|
|
||||||
https://github.com/restic/restic/pull/2709
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Enhancement: Improve speed of check command
|
|
||||||
|
|
||||||
We've improved the check command to traverse trees only once independent of
|
|
||||||
whether they are contained in multiple snapshots. The check command is now much
|
|
||||||
faster for repositories with a large number of snapshots.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2328
|
|
||||||
https://github.com/restic/restic/issues/2284
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
Change: Return exit code 3 when failing to backup all source data
|
|
||||||
|
|
||||||
The backup command used to return a zero exit code as long as a snapshot
|
|
||||||
could be created successfully, even if some of the source files could not
|
|
||||||
be read (in which case the snapshot would contain the rest of the files).
|
|
||||||
|
|
||||||
This made it hard for automation/scripts to detect failures/incomplete
|
|
||||||
backups by looking at the exit code. Restic now returns the following exit
|
|
||||||
codes for the backup command:
|
|
||||||
|
|
||||||
- 0 when the command was successful
|
|
||||||
- 1 when there was a fatal error (no snapshot created)
|
|
||||||
- 3 when some source data could not be read (incomplete snapshot created)
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2546
|
|
||||||
https://github.com/restic/restic/issues/956
|
|
||||||
https://github.com/restic/restic/issues/2064
|
|
||||||
https://github.com/restic/restic/issues/2526
|
|
||||||
https://github.com/restic/restic/issues/2364
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Enhancement: Improve speed of diff command
|
|
||||||
|
|
||||||
We've improved the performance of the diff command when comparing snapshots
|
|
||||||
with similar content. It should run up to twice as fast as before.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2598
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Enhancement: Slightly reduce memory usage of prune and stats commands
|
|
||||||
|
|
||||||
The prune and the stats command kept directory identifiers in memory twice
|
|
||||||
while searching for used blobs.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2599
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
Change: Update dependencies, require Go >= 1.13
|
|
||||||
|
|
||||||
Restic now requires Go to be at least 1.13. This allows simplifications in the
|
|
||||||
build process and removing workarounds.
|
|
||||||
|
|
||||||
This is also probably the last version of restic still supporting mounting
|
|
||||||
repositories via fuse on macOS. The library we're using for fuse does not
|
|
||||||
support macOS any more and osxfuse is not open source any more.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2600
|
|
||||||
https://github.com/restic/restic/pull/2852
|
|
||||||
https://github.com/restic/restic/pull/2927
|
|
||||||
https://github.com/bazil/fuse/issues/224
|
|
||||||
https://github.com/osxfuse/osxfuse/issues/590
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Bugfix: Add stricter prune error checks
|
|
||||||
|
|
||||||
Additional checks were added to the prune command in order to improve
|
|
||||||
resiliency to backend, hardware and/or networking issues. The checks now
|
|
||||||
detect a few more cases where such outside factors could potentially cause
|
|
||||||
data loss.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2674
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Enhancement: S3 backend: Add support for WebIdentityTokenFile
|
|
||||||
|
|
||||||
We've added support for EKS IAM roles for service accounts feature to the S3 backend.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2733
|
|
||||||
https://github.com/restic/restic/issues/2703
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Enhancement: Optimize handling of new index entries
|
|
||||||
|
|
||||||
Restic now uses less memory for backups which add a lot of data, e.g. large initial backups.
|
|
||||||
In addition, we've improved the stability in some edge cases.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2773
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Enhancement: Reduce memory consumption of in-memory index
|
|
||||||
|
|
||||||
We've improved how the index is stored in memory.
|
|
||||||
This change can reduce memory usage for large repositories by up to 50%
|
|
||||||
(depending on the operation).
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2781
|
|
||||||
https://github.com/restic/restic/pull/2812
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Enhancement: Optimize `list blobs` command
|
|
||||||
|
|
||||||
We've changed the implementation of `list blobs` which should be now a bit faster
|
|
||||||
and consume almost no memory even for large repositories.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2786
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Enhancement: Optimized file access in restic mount
|
|
||||||
|
|
||||||
Reading large (> 100GiB) files from restic mountpoints is now faster,
|
|
||||||
and the speedup is greater for larger files.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2790
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Change: Honor the --no-lock flag in the mount command
|
|
||||||
|
|
||||||
The mount command now does not lock the repository if given the
|
|
||||||
--no-lock flag. This allows to mount repositories which are archived
|
|
||||||
on a read only backend/filesystem.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/1597
|
|
||||||
https://github.com/restic/restic/pull/2821
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Enhancement: Speed-up file deletion in forget, prune and rebuild-index
|
|
||||||
|
|
||||||
We've sped up the file deletion for the commands forget, prune and
|
|
||||||
rebuild-index, especially for remote repositories.
|
|
||||||
Deletion was sequential before and is now run in parallel.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2840
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Bugfix: Fix possible crash in the progress bar of check --read-data
|
|
||||||
|
|
||||||
We've fixed a possible crash while displaying the progress bar for the
|
|
||||||
check --read-data command. The crash occurred when the length of the
|
|
||||||
progress bar status exceeded the terminal width, which only happened for
|
|
||||||
very narrow terminal windows.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2899
|
|
||||||
https://forum.restic.net/t/restic-rclone-pcloud-connection-issues/2963/15
|
|
||||||
@@ -7,7 +7,7 @@ vulnerability, but urge all users to upgrade to the latest version of restic.
|
|||||||
|
|
||||||
Exploiting the vulnerability requires a Linux/Unix system which saves backups
|
Exploiting the vulnerability requires a Linux/Unix system which saves backups
|
||||||
via restic and a Windows systems which restores files from the repo. In
|
via restic and a Windows systems which restores files from the repo. In
|
||||||
addition, the attackers need to be able to create files with arbitrary
|
addition, the attackers need to be able to create create files with arbitrary
|
||||||
names which are then saved to the restic repo. For example, by creating a file
|
names which are then saved to the restic repo. For example, by creating a file
|
||||||
named "..\test.txt" (which is a perfectly legal filename on Linux) and
|
named "..\test.txt" (which is a perfectly legal filename on Linux) and
|
||||||
restoring a snapshot containing this file on Windows, it would be written to
|
restoring a snapshot containing this file on Windows, it would be written to
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -14,10 +14,4 @@ file can be written to the file before any of the preceeding file blobs.
|
|||||||
It is therefore possible to have gaps in the data written to the target
|
It is therefore possible to have gaps in the data written to the target
|
||||||
files if restore fails or interrupted by the user.
|
files if restore fails or interrupted by the user.
|
||||||
|
|
||||||
The implementation will try to preallocate space for the restored files
|
|
||||||
on the filesystem to prevent file fragmentation. This ensures good read
|
|
||||||
performance for large files, like for example VM images. If preallocating
|
|
||||||
space is not supported by the filesystem, then this step is silently skipped.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2195
|
https://github.com/restic/restic/pull/2195
|
||||||
https://github.com/restic/restic/pull/2893
|
|
||||||
File renamed without changes.
@@ -1,9 +1,7 @@
|
|||||||
Enhancement: Improve the chunking algorithm
|
Enhancement: Improve the chunking algorithm
|
||||||
|
|
||||||
We've updated the chunker library responsible for splitting files into smaller
|
We've updated the chunker library responsible for splitting files into smaller
|
||||||
blocks. It should improve the chunking throughput by 5-15% depending on the
|
blocks. It should improve the chunking throughput by 5-10% depending on the
|
||||||
CPU.
|
CPU.
|
||||||
|
|
||||||
https://github.com/restic/restic/pull/2576
|
https://github.com/restic/restic/pull/2576
|
||||||
https://github.com/restic/restic/pull/2845
|
|
||||||
https://github.com/restic/restic/issues/2820
|
|
||||||
File renamed without changes.
@@ -0,0 +1,6 @@
|
|||||||
|
Change: Require Go >= 1.11
|
||||||
|
|
||||||
|
Restic now requires Go to be at least 1.11. This allows simplifications in the
|
||||||
|
build process and removing workarounds.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/2600
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,25 @@
|
|||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: linux
|
||||||
|
go: "1.9.x"
|
||||||
|
- os: linux
|
||||||
|
go: "1.10.x"
|
||||||
|
- os: linux
|
||||||
|
go: "tip"
|
||||||
|
- os: osx
|
||||||
|
go: "1.10.x"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get -t ./...
|
||||||
|
- go get -u golang.org/x/lint/golint
|
||||||
|
- go get -u golang.org/x/tools/cmd/goimports
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go vet ./...
|
||||||
|
- go test -v -cpu=2 ./...
|
||||||
|
- go test -v -cpu=1,2,4 -short -race ./...
|
||||||
|
- diff -au <(goimports -d .) <(printf "")
|
||||||
|
- diff -au <(golint ./...) <(printf "")
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
Copyright (c) 2014, Alexander Neumann <alexander@bumpern.de>
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[](http://godoc.org/github.com/restic/chunker)
|
||||||
|
[](https://travis-ci.com/restic/chunker)
|
||||||
|
|
||||||
|
The package `chunker` implements content-defined-chunking (CDC) based on a
|
||||||
|
rolling Rabin Hash. The library is part of the [restic backup
|
||||||
|
program](https://github.com/restic/restic).
|
||||||
|
|
||||||
|
An introduction to Content Defined Chunking can be found in the restic blog
|
||||||
|
post [Foundation - Introducing Content Defined Chunking (CDC)](https://restic.github.io/blog/2015-09-12/restic-foundation1-cdc).
|
||||||
|
|
||||||
|
You can find the API documentation at
|
||||||
|
https://godoc.org/github.com/restic/chunker
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
package chunker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
kiB = 1024
|
||||||
|
miB = 1024 * kiB
|
||||||
|
|
||||||
|
// WindowSize is the size of the sliding window.
|
||||||
|
windowSize = 64
|
||||||
|
|
||||||
|
// MinSize is the default minimal size of a chunk.
|
||||||
|
MinSize = 512 * kiB
|
||||||
|
// MaxSize is the default maximal size of a chunk.
|
||||||
|
MaxSize = 8 * miB
|
||||||
|
|
||||||
|
chunkerBufSize = 512 * kiB
|
||||||
|
)
|
||||||
|
|
||||||
|
type tables struct {
|
||||||
|
out [256]Pol
|
||||||
|
mod [256]Pol
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache precomputed tables, these are read-only anyway
|
||||||
|
var cache struct {
|
||||||
|
entries map[Pol]tables
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cache.entries = make(map[Pol]tables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk is one content-dependent chunk of bytes whose end was cut when the
|
||||||
|
// Rabin Fingerprint had the value stored in Cut.
|
||||||
|
type Chunk struct {
|
||||||
|
Start uint
|
||||||
|
Length uint
|
||||||
|
Cut uint64
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type chunkerState struct {
|
||||||
|
window [windowSize]byte
|
||||||
|
wpos uint
|
||||||
|
|
||||||
|
buf []byte
|
||||||
|
bpos uint
|
||||||
|
bmax uint
|
||||||
|
|
||||||
|
start uint
|
||||||
|
count uint
|
||||||
|
pos uint
|
||||||
|
|
||||||
|
pre uint // wait for this many bytes before start calculating an new chunk
|
||||||
|
|
||||||
|
digest uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type chunkerConfig struct {
|
||||||
|
MinSize, MaxSize uint
|
||||||
|
|
||||||
|
pol Pol
|
||||||
|
polShift uint
|
||||||
|
tables tables
|
||||||
|
tablesInitialized bool
|
||||||
|
splitmask uint64
|
||||||
|
|
||||||
|
rd io.Reader
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunker splits content with Rabin Fingerprints.
|
||||||
|
type Chunker struct {
|
||||||
|
chunkerConfig
|
||||||
|
chunkerState
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAverageBits allows to control the frequency of chunk discovery:
|
||||||
|
// the lower averageBits, the higher amount of chunks will be identified.
|
||||||
|
// The default value is 20 bits, so chunks will be of 1MiB size on average.
|
||||||
|
func (c *Chunker) SetAverageBits(averageBits int) {
|
||||||
|
c.splitmask = (1 << uint64(averageBits)) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Chunker based on polynomial p that reads from rd.
|
||||||
|
func New(rd io.Reader, pol Pol) *Chunker {
|
||||||
|
return NewWithBoundaries(rd, pol, MinSize, MaxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithBoundaries returns a new Chunker based on polynomial p that reads from
|
||||||
|
// rd and custom min and max size boundaries.
|
||||||
|
func NewWithBoundaries(rd io.Reader, pol Pol, min, max uint) *Chunker {
|
||||||
|
c := &Chunker{
|
||||||
|
chunkerState: chunkerState{
|
||||||
|
buf: make([]byte, chunkerBufSize),
|
||||||
|
},
|
||||||
|
chunkerConfig: chunkerConfig{
|
||||||
|
pol: pol,
|
||||||
|
rd: rd,
|
||||||
|
MinSize: min,
|
||||||
|
MaxSize: max,
|
||||||
|
splitmask: (1 << 20) - 1, // aim to create chunks of 20 bits or about 1MiB on average.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.reset()
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset reinitializes the chunker with a new reader and polynomial.
|
||||||
|
func (c *Chunker) Reset(rd io.Reader, pol Pol) {
|
||||||
|
c.ResetWithBoundaries(rd, pol, MinSize, MaxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetWithBoundaries reinitializes the chunker with a new reader, polynomial
|
||||||
|
// and custom min and max size boundaries.
|
||||||
|
func (c *Chunker) ResetWithBoundaries(rd io.Reader, pol Pol, min, max uint) {
|
||||||
|
*c = Chunker{
|
||||||
|
chunkerState: chunkerState{
|
||||||
|
buf: c.buf,
|
||||||
|
},
|
||||||
|
chunkerConfig: chunkerConfig{
|
||||||
|
pol: pol,
|
||||||
|
rd: rd,
|
||||||
|
MinSize: min,
|
||||||
|
MaxSize: max,
|
||||||
|
splitmask: (1 << 20) - 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chunker) reset() {
|
||||||
|
c.polShift = uint(c.pol.Deg() - 8)
|
||||||
|
c.fillTables()
|
||||||
|
|
||||||
|
for i := 0; i < windowSize; i++ {
|
||||||
|
c.window[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
c.closed = false
|
||||||
|
c.digest = 0
|
||||||
|
c.wpos = 0
|
||||||
|
c.count = 0
|
||||||
|
c.digest = c.slide(c.digest, 1)
|
||||||
|
c.start = c.pos
|
||||||
|
|
||||||
|
// do not start a new chunk unless at least MinSize bytes have been read
|
||||||
|
c.pre = c.MinSize - windowSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillTables calculates out_table and mod_table for optimization. This
|
||||||
|
// implementation uses a cache in the global variable cache.
|
||||||
|
func (c *Chunker) fillTables() {
|
||||||
|
// if polynomial hasn't been specified, do not compute anything for now
|
||||||
|
if c.pol == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tablesInitialized = true
|
||||||
|
|
||||||
|
// test if the tables are cached for this polynomial
|
||||||
|
cache.Lock()
|
||||||
|
defer cache.Unlock()
|
||||||
|
if t, ok := cache.entries[c.pol]; ok {
|
||||||
|
c.tables = t
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate table for sliding out bytes. The byte to slide out is used as
|
||||||
|
// the index for the table, the value contains the following:
|
||||||
|
// out_table[b] = Hash(b || 0 || ... || 0)
|
||||||
|
// \ windowsize-1 zero bytes /
|
||||||
|
// To slide out byte b_0 for window size w with known hash
|
||||||
|
// H := H(b_0 || ... || b_w), it is sufficient to add out_table[b_0]:
|
||||||
|
// H(b_0 || ... || b_w) + H(b_0 || 0 || ... || 0)
|
||||||
|
// = H(b_0 + b_0 || b_1 + 0 || ... || b_w + 0)
|
||||||
|
// = H( 0 || b_1 || ... || b_w)
|
||||||
|
//
|
||||||
|
// Afterwards a new byte can be shifted in.
|
||||||
|
for b := 0; b < 256; b++ {
|
||||||
|
var h Pol
|
||||||
|
|
||||||
|
h = appendByte(h, byte(b), c.pol)
|
||||||
|
for i := 0; i < windowSize-1; i++ {
|
||||||
|
h = appendByte(h, 0, c.pol)
|
||||||
|
}
|
||||||
|
c.tables.out[b] = h
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate table for reduction mod Polynomial
|
||||||
|
k := c.pol.Deg()
|
||||||
|
for b := 0; b < 256; b++ {
|
||||||
|
// mod_table[b] = A | B, where A = (b(x) * x^k mod pol) and B = b(x) * x^k
|
||||||
|
//
|
||||||
|
// The 8 bits above deg(Polynomial) determine what happens next and so
|
||||||
|
// these bits are used as a lookup to this table. The value is split in
|
||||||
|
// two parts: Part A contains the result of the modulus operation, part
|
||||||
|
// B is used to cancel out the 8 top bits so that one XOR operation is
|
||||||
|
// enough to reduce modulo Polynomial
|
||||||
|
c.tables.mod[b] = Pol(uint64(b)<<uint(k)).Mod(c.pol) | (Pol(b) << uint(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.entries[c.pol] = c.tables
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the position and length of the next chunk of data. If an error
|
||||||
|
// occurs while reading, the error is returned. Afterwards, the state of the
|
||||||
|
// current chunk is undefined. When the last chunk has been returned, all
|
||||||
|
// subsequent calls yield an io.EOF error.
|
||||||
|
func (c *Chunker) Next(data []byte) (Chunk, error) {
|
||||||
|
data = data[:0]
|
||||||
|
if !c.tablesInitialized {
|
||||||
|
return Chunk{}, errors.New("tables for polynomial computation not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
tabout := c.tables.out
|
||||||
|
tabmod := c.tables.mod
|
||||||
|
polShift := c.polShift
|
||||||
|
// go guarantees the expected behavior for bit shifts even for shift counts
|
||||||
|
// larger than the value width. Bounding the value of polShift allows the compiler
|
||||||
|
// to optimize the code for 'digest >> polShift'
|
||||||
|
if polShift > 53-8 {
|
||||||
|
return Chunk{}, errors.New("the polynomial must have a degree less than or equal 53")
|
||||||
|
}
|
||||||
|
minSize := c.MinSize
|
||||||
|
maxSize := c.MaxSize
|
||||||
|
buf := c.buf
|
||||||
|
for {
|
||||||
|
if c.bpos >= c.bmax {
|
||||||
|
n, err := io.ReadFull(c.rd, buf[:])
|
||||||
|
|
||||||
|
if err == io.ErrUnexpectedEOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// io.ReadFull only returns io.EOF when no bytes could be read. If
|
||||||
|
// this is the case and we're in this branch, there are no more
|
||||||
|
// bytes to buffer, so this was the last chunk. If a different
|
||||||
|
// error has occurred, return that error and abandon the current
|
||||||
|
// chunk.
|
||||||
|
if err == io.EOF && !c.closed {
|
||||||
|
c.closed = true
|
||||||
|
|
||||||
|
// return current chunk, if any bytes have been processed
|
||||||
|
if c.count > 0 {
|
||||||
|
return Chunk{
|
||||||
|
Start: c.start,
|
||||||
|
Length: c.count,
|
||||||
|
Cut: c.digest,
|
||||||
|
Data: data,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Chunk{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 0 {
|
||||||
|
return Chunk{}, fmt.Errorf("ReadFull returned negative number of bytes read: %v", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.bpos = 0
|
||||||
|
c.bmax = uint(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if bytes have to be dismissed before starting a new chunk
|
||||||
|
if c.pre > 0 {
|
||||||
|
n := c.bmax - c.bpos
|
||||||
|
if c.pre > uint(n) {
|
||||||
|
c.pre -= uint(n)
|
||||||
|
data = append(data, buf[c.bpos:c.bmax]...)
|
||||||
|
|
||||||
|
c.count += uint(n)
|
||||||
|
c.pos += uint(n)
|
||||||
|
c.bpos = c.bmax
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, buf[c.bpos:c.bpos+c.pre]...)
|
||||||
|
|
||||||
|
c.bpos += c.pre
|
||||||
|
c.count += c.pre
|
||||||
|
c.pos += c.pre
|
||||||
|
c.pre = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
add := c.count
|
||||||
|
digest := c.digest
|
||||||
|
win := c.window
|
||||||
|
wpos := c.wpos
|
||||||
|
for _, b := range buf[c.bpos:c.bmax] {
|
||||||
|
// slide(b)
|
||||||
|
// limit wpos before to elide array bound checks
|
||||||
|
wpos = wpos % windowSize
|
||||||
|
out := win[wpos]
|
||||||
|
win[wpos] = b
|
||||||
|
digest ^= uint64(tabout[out])
|
||||||
|
wpos++
|
||||||
|
|
||||||
|
// updateDigest
|
||||||
|
index := byte(digest >> polShift)
|
||||||
|
digest <<= 8
|
||||||
|
digest |= uint64(b)
|
||||||
|
|
||||||
|
digest ^= uint64(tabmod[index])
|
||||||
|
// end manual inline
|
||||||
|
|
||||||
|
add++
|
||||||
|
if add < minSize {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digest&c.splitmask) == 0 || add >= maxSize {
|
||||||
|
i := add - c.count - 1
|
||||||
|
data = append(data, c.buf[c.bpos:c.bpos+uint(i)+1]...)
|
||||||
|
c.count = add
|
||||||
|
c.pos += uint(i) + 1
|
||||||
|
c.bpos += uint(i) + 1
|
||||||
|
c.buf = buf
|
||||||
|
|
||||||
|
chunk := Chunk{
|
||||||
|
Start: c.start,
|
||||||
|
Length: c.count,
|
||||||
|
Cut: digest,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.reset()
|
||||||
|
|
||||||
|
return chunk, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.digest = digest
|
||||||
|
c.window = win
|
||||||
|
c.wpos = wpos % windowSize
|
||||||
|
|
||||||
|
steps := c.bmax - c.bpos
|
||||||
|
if steps > 0 {
|
||||||
|
data = append(data, c.buf[c.bpos:c.bpos+steps]...)
|
||||||
|
}
|
||||||
|
c.count += steps
|
||||||
|
c.pos += steps
|
||||||
|
c.bpos = c.bmax
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDigest(digest uint64, polShift uint, tab tables, b byte) (newDigest uint64) {
|
||||||
|
index := digest >> polShift
|
||||||
|
digest <<= 8
|
||||||
|
digest |= uint64(b)
|
||||||
|
|
||||||
|
digest ^= uint64(tab.mod[index])
|
||||||
|
return digest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chunker) slide(digest uint64, b byte) (newDigest uint64) {
|
||||||
|
out := c.window[c.wpos]
|
||||||
|
c.window[c.wpos] = b
|
||||||
|
digest ^= uint64(c.tables.out[out])
|
||||||
|
c.wpos = (c.wpos + 1) % windowSize
|
||||||
|
|
||||||
|
digest = updateDigest(digest, c.polShift, c.tables, b)
|
||||||
|
return digest
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendByte(hash Pol, b byte, pol Pol) Pol {
|
||||||
|
hash <<= 8
|
||||||
|
hash |= Pol(b)
|
||||||
|
|
||||||
|
return hash.Mod(pol)
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
package chunker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseDigest(s string) []byte {
|
||||||
|
d, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
type chunk struct {
|
||||||
|
Length uint
|
||||||
|
CutFP uint64
|
||||||
|
Digest []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// polynomial used for all the tests below
|
||||||
|
const testPol = Pol(0x3DA3358B4DC173)
|
||||||
|
|
||||||
|
// created for 32MB of random data out of math/rand's Uint32() seeded by
|
||||||
|
// constant 23
|
||||||
|
//
|
||||||
|
// chunking configuration:
|
||||||
|
// window size 64, avg chunksize 1<<20, min chunksize 1<<19, max chunksize 1<<23
|
||||||
|
// polynom 0x3DA3358B4DC173
|
||||||
|
var chunks1 = []chunk{
|
||||||
|
chunk{2163460, 0x000b98d4cdf00000, parseDigest("4b94cb2cf293855ea43bf766731c74969b91aa6bf3c078719aabdd19860d590d")},
|
||||||
|
chunk{643703, 0x000d4e8364d00000, parseDigest("5727a63c0964f365ab8ed2ccf604912f2ea7be29759a2b53ede4d6841e397407")},
|
||||||
|
chunk{1528956, 0x0015a25c2ef00000, parseDigest("a73759636a1e7a2758767791c69e81b69fb49236c6929e5d1b654e06e37674ba")},
|
||||||
|
chunk{1955808, 0x00102a8242e00000, parseDigest("c955fb059409b25f07e5ae09defbbc2aadf117c97a3724e06ad4abd2787e6824")},
|
||||||
|
chunk{2222372, 0x00045da878000000, parseDigest("6ba5e9f7e1b310722be3627716cf469be941f7f3e39a4c3bcefea492ec31ee56")},
|
||||||
|
chunk{2538687, 0x00198a8179900000, parseDigest("8687937412f654b5cfe4a82b08f28393a0c040f77c6f95e26742c2fc4254bfde")},
|
||||||
|
chunk{609606, 0x001d4e8d17100000, parseDigest("5da820742ff5feb3369112938d3095785487456f65a8efc4b96dac4be7ebb259")},
|
||||||
|
chunk{1205738, 0x000a7204dd600000, parseDigest("cc70d8fad5472beb031b1aca356bcab86c7368f40faa24fe5f8922c6c268c299")},
|
||||||
|
chunk{959742, 0x00183e71e1400000, parseDigest("4065bdd778f95676c92b38ac265d361f81bff17d76e5d9452cf985a2ea5a4e39")},
|
||||||
|
chunk{4036109, 0x001fec043c700000, parseDigest("b9cf166e75200eb4993fc9b6e22300a6790c75e6b0fc8f3f29b68a752d42f275")},
|
||||||
|
chunk{1525894, 0x000b1574b1500000, parseDigest("2f238180e4ca1f7520a05f3d6059233926341090f9236ce677690c1823eccab3")},
|
||||||
|
chunk{1352720, 0x00018965f2e00000, parseDigest("afd12f13286a3901430de816e62b85cc62468c059295ce5888b76b3af9028d84")},
|
||||||
|
chunk{811884, 0x00155628aa100000, parseDigest("42d0cdb1ee7c48e552705d18e061abb70ae7957027db8ae8db37ec756472a70a")},
|
||||||
|
chunk{1282314, 0x001909a0a1400000, parseDigest("819721c2457426eb4f4c7565050c44c32076a56fa9b4515a1c7796441730eb58")},
|
||||||
|
chunk{1318021, 0x001cceb980000000, parseDigest("842eb53543db55bacac5e25cb91e43cc2e310fe5f9acc1aee86bdf5e91389374")},
|
||||||
|
chunk{948640, 0x0011f7a470a00000, parseDigest("b8e36bf7019bb96ac3fb7867659d2167d9d3b3148c09fe0de45850b8fe577185")},
|
||||||
|
chunk{645464, 0x00030ce2d9400000, parseDigest("5584bd27982191c3329f01ed846bfd266e96548dfa87018f745c33cfc240211d")},
|
||||||
|
chunk{533758, 0x0004435c53c00000, parseDigest("4da778a25b72a9a0d53529eccfe2e5865a789116cb1800f470d8df685a8ab05d")},
|
||||||
|
chunk{1128303, 0x0000c48517800000, parseDigest("08c6b0b38095b348d80300f0be4c5184d2744a17147c2cba5cc4315abf4c048f")},
|
||||||
|
chunk{800374, 0x000968473f900000, parseDigest("820284d2c8fd243429674c996d8eb8d3450cbc32421f43113e980f516282c7bf")},
|
||||||
|
chunk{2453512, 0x001e197c92600000, parseDigest("5fa870ed107c67704258e5e50abe67509fb73562caf77caa843b5f243425d853")},
|
||||||
|
chunk{2651975, 0x000ae6c868000000, parseDigest("181347d2bbec32bef77ad5e9001e6af80f6abcf3576549384d334ee00c1988d8")},
|
||||||
|
chunk{237392, 0x0000000000000001, parseDigest("fcd567f5d866357a8e299fd5b2359bb2c8157c30395229c4e9b0a353944a7978")},
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if nullbytes are correctly split, even if length is a multiple of MinSize.
|
||||||
|
var chunks2 = []chunk{
|
||||||
|
chunk{MinSize, 0, parseDigest("07854d2fef297a06ba81685e660c332de36d5d18d546927d30daad6d7fda1541")},
|
||||||
|
chunk{MinSize, 0, parseDigest("07854d2fef297a06ba81685e660c332de36d5d18d546927d30daad6d7fda1541")},
|
||||||
|
chunk{MinSize, 0, parseDigest("07854d2fef297a06ba81685e660c332de36d5d18d546927d30daad6d7fda1541")},
|
||||||
|
chunk{MinSize, 0, parseDigest("07854d2fef297a06ba81685e660c332de36d5d18d546927d30daad6d7fda1541")},
|
||||||
|
}
|
||||||
|
|
||||||
|
// the same as chunks1, but avg chunksize is 1<<19
|
||||||
|
var chunks3 = []chunk{
|
||||||
|
chunk{1491586, 0x00023e586ea80000, parseDigest("4c008237df602048039287427171cef568a6cb965d1b5ca28dc80504a24bb061")},
|
||||||
|
chunk{671874, 0x000b98d4cdf00000, parseDigest("fa8a42321b90c3d4ce9dd850562b2fd0c0fe4bdd26cf01a24f22046a224225d3")},
|
||||||
|
chunk{643703, 0x000d4e8364d00000, parseDigest("5727a63c0964f365ab8ed2ccf604912f2ea7be29759a2b53ede4d6841e397407")},
|
||||||
|
chunk{1284146, 0x0012b527e4780000, parseDigest("16d04cafecbeae9eaedd49da14c7ad7cdc2b1cc8569e5c16c32c9fb045aa899a")},
|
||||||
|
chunk{823366, 0x000d1d6752180000, parseDigest("48662c118514817825ad4761e8e2e5f28f9bd8281b07e95dcafc6d02e0aa45c3")},
|
||||||
|
chunk{810134, 0x0016071b6e180000, parseDigest("f629581aa05562f97f2c359890734c8574c5575da32f9289c5ba70bfd05f3f46")},
|
||||||
|
chunk{567118, 0x00102a8242e00000, parseDigest("d4f0797c56c60d01bac33bfd49957a4816b6c067fc155b026de8a214cab4d70a")},
|
||||||
|
chunk{821315, 0x001b3e42c8180000, parseDigest("8ebd0fd5db0293bd19140da936eb8b1bbd3cd6ffbec487385b956790014751ca")},
|
||||||
|
chunk{1401057, 0x00045da878000000, parseDigest("001360af59adf4871ef138cfa2bb49007e86edaf5ac2d6f0b3d3014510991848")},
|
||||||
|
chunk{2311122, 0x0005cbd885380000, parseDigest("8276d489b566086d9da95dc5c5fe6fc7d72646dd3308ced6b5b6ddb8595f0aa1")},
|
||||||
|
chunk{608723, 0x001cfcd86f280000, parseDigest("518db33ba6a79d4f3720946f3785c05b9611082586d47ea58390fc2f6de9449e")},
|
||||||
|
chunk{980456, 0x0013edb7a7f80000, parseDigest("0121b1690738395e15fecba1410cd0bf13fde02225160cad148829f77e7b6c99")},
|
||||||
|
chunk{1140278, 0x0001f9f017e80000, parseDigest("28ca7c74804b5075d4f5eeb11f0845d99f62e8ea3a42b9a05c7bd5f2fca619dd")},
|
||||||
|
chunk{2015542, 0x00097bf5d8180000, parseDigest("6fe8291f427d48650a5f0f944305d3a2dbc649bd401d2655fc0bdd42e890ca5a")},
|
||||||
|
chunk{904752, 0x000e1863eff80000, parseDigest("62af1f1eb3f588d18aff28473303cc4731fc3cafcc52ce818fee3c4c2820854d")},
|
||||||
|
chunk{713072, 0x001f3bb1b9b80000, parseDigest("4bda9dc2e3031d004d87a5cc93fe5207c4b0843186481b8f31597dc6ffa1496c")},
|
||||||
|
chunk{675937, 0x001fec043c700000, parseDigest("5299c8c5acec1b90bb020cd75718aab5e12abb9bf66291465fd10e6a823a8b4a")},
|
||||||
|
chunk{1525894, 0x000b1574b1500000, parseDigest("2f238180e4ca1f7520a05f3d6059233926341090f9236ce677690c1823eccab3")},
|
||||||
|
chunk{1352720, 0x00018965f2e00000, parseDigest("afd12f13286a3901430de816e62b85cc62468c059295ce5888b76b3af9028d84")},
|
||||||
|
chunk{811884, 0x00155628aa100000, parseDigest("42d0cdb1ee7c48e552705d18e061abb70ae7957027db8ae8db37ec756472a70a")},
|
||||||
|
chunk{1282314, 0x001909a0a1400000, parseDigest("819721c2457426eb4f4c7565050c44c32076a56fa9b4515a1c7796441730eb58")},
|
||||||
|
chunk{1093738, 0x0017f5d048880000, parseDigest("5dddfa7a241b68f65d267744bdb082ee865f3c2f0d8b946ea0ee47868a01bbff")},
|
||||||
|
chunk{962003, 0x000b921f7ef80000, parseDigest("0cb5c9ebba196b441c715c8d805f6e7143a81cd5b0d2c65c6aacf59ca9124af9")},
|
||||||
|
chunk{856384, 0x00030ce2d9400000, parseDigest("7734b206d46f3f387e8661e81edf5b1a91ea681867beb5831c18aaa86632d7fb")},
|
||||||
|
chunk{533758, 0x0004435c53c00000, parseDigest("4da778a25b72a9a0d53529eccfe2e5865a789116cb1800f470d8df685a8ab05d")},
|
||||||
|
chunk{1128303, 0x0000c48517800000, parseDigest("08c6b0b38095b348d80300f0be4c5184d2744a17147c2cba5cc4315abf4c048f")},
|
||||||
|
chunk{800374, 0x000968473f900000, parseDigest("820284d2c8fd243429674c996d8eb8d3450cbc32421f43113e980f516282c7bf")},
|
||||||
|
chunk{2453512, 0x001e197c92600000, parseDigest("5fa870ed107c67704258e5e50abe67509fb73562caf77caa843b5f243425d853")},
|
||||||
|
chunk{665901, 0x00118c842cb80000, parseDigest("deceec26163842fdef6560311c69bf8a9871a56e16d719e2c4b7e4d668ceb61f")},
|
||||||
|
chunk{1986074, 0x000ae6c868000000, parseDigest("64cd64bf3c3bc389eb20df8310f0427d1c36ab2eaaf09e346bfa7f0453fc1a18")},
|
||||||
|
chunk{237392, 0x0000000000000001, parseDigest("fcd567f5d866357a8e299fd5b2359bb2c8157c30395229c4e9b0a353944a7978")},
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWithData(t *testing.T, chnker *Chunker, testChunks []chunk, checkDigest bool) []Chunk {
|
||||||
|
chunks := []Chunk{}
|
||||||
|
|
||||||
|
pos := uint(0)
|
||||||
|
for i, chunk := range testChunks {
|
||||||
|
c, err := chnker.Next(nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error returned with chunk %d: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Start != pos {
|
||||||
|
t.Fatalf("Start for chunk %d does not match: expected %d, got %d",
|
||||||
|
i, pos, c.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Length != chunk.Length {
|
||||||
|
t.Fatalf("Length for chunk %d does not match: expected %d, got %d",
|
||||||
|
i, chunk.Length, c.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Cut != chunk.CutFP {
|
||||||
|
t.Fatalf("Cut fingerprint for chunk %d/%d does not match: expected %016x, got %016x",
|
||||||
|
i, len(chunks)-1, chunk.CutFP, c.Cut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkDigest {
|
||||||
|
digest := hashData(c.Data)
|
||||||
|
if !bytes.Equal(chunk.Digest, digest) {
|
||||||
|
t.Fatalf("Digest fingerprint for chunk %d/%d does not match: expected %02x, got %02x",
|
||||||
|
i, len(chunks)-1, chunk.Digest, digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += c.Length
|
||||||
|
chunks = append(chunks, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := chnker.Next(nil)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Fatal("Wrong error returned after last chunk")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chunks) != len(testChunks) {
|
||||||
|
t.Fatal("Amounts of test and resulting chunks do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandom(seed int64, count int) []byte {
|
||||||
|
buf := make([]byte, count)
|
||||||
|
|
||||||
|
rnd := rand.New(rand.NewSource(seed))
|
||||||
|
for i := 0; i < count; i += 4 {
|
||||||
|
r := rnd.Uint32()
|
||||||
|
buf[i] = byte(r)
|
||||||
|
buf[i+1] = byte(r >> 8)
|
||||||
|
buf[i+2] = byte(r >> 16)
|
||||||
|
buf[i+3] = byte(r >> 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashData(d []byte) []byte {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(d)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunker(t *testing.T) {
|
||||||
|
// setup data source
|
||||||
|
buf := getRandom(23, 32*1024*1024)
|
||||||
|
ch := New(bytes.NewReader(buf), testPol)
|
||||||
|
testWithData(t, ch, chunks1, true)
|
||||||
|
|
||||||
|
// setup nullbyte data source
|
||||||
|
buf = bytes.Repeat([]byte{0}, len(chunks2)*MinSize)
|
||||||
|
ch = New(bytes.NewReader(buf), testPol)
|
||||||
|
|
||||||
|
testWithData(t, ch, chunks2, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunkerWithCustomAverageBits(t *testing.T) {
|
||||||
|
buf := getRandom(23, 32*1024*1024)
|
||||||
|
ch := New(bytes.NewReader(buf), testPol)
|
||||||
|
|
||||||
|
// sligthly decrease averageBits to get more chunks
|
||||||
|
ch.SetAverageBits(19)
|
||||||
|
testWithData(t, ch, chunks3, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunkerReset(t *testing.T) {
|
||||||
|
buf := getRandom(23, 32*1024*1024)
|
||||||
|
ch := New(bytes.NewReader(buf), testPol)
|
||||||
|
testWithData(t, ch, chunks1, true)
|
||||||
|
|
||||||
|
ch.Reset(bytes.NewReader(buf), testPol)
|
||||||
|
testWithData(t, ch, chunks1, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunkerWithRandomPolynomial(t *testing.T) {
|
||||||
|
// setup data source
|
||||||
|
buf := getRandom(23, 32*1024*1024)
|
||||||
|
|
||||||
|
// generate a new random polynomial
|
||||||
|
start := time.Now()
|
||||||
|
p, err := RandomPolynomial()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("generating random polynomial took %v", time.Since(start))
|
||||||
|
|
||||||
|
start = time.Now()
|
||||||
|
ch := New(bytes.NewReader(buf), p)
|
||||||
|
t.Logf("creating chunker took %v", time.Since(start))
|
||||||
|
|
||||||
|
// make sure that first chunk is different
|
||||||
|
c, err := ch.Next(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Cut == chunks1[0].CutFP {
|
||||||
|
t.Fatal("Cut point is the same")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Length == chunks1[0].Length {
|
||||||
|
t.Fatal("Length is the same")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(hashData(c.Data), chunks1[0].Digest) {
|
||||||
|
t.Fatal("Digest is the same")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunkerWithoutHash(t *testing.T) {
|
||||||
|
// setup data source
|
||||||
|
buf := getRandom(23, 32*1024*1024)
|
||||||
|
|
||||||
|
ch := New(bytes.NewReader(buf), testPol)
|
||||||
|
chunks := testWithData(t, ch, chunks1, false)
|
||||||
|
|
||||||
|
// test reader
|
||||||
|
for i, c := range chunks {
|
||||||
|
if uint(len(c.Data)) != chunks1[i].Length {
|
||||||
|
t.Fatalf("reader returned wrong number of bytes: expected %d, got %d",
|
||||||
|
chunks1[i].Length, len(c.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf[c.Start:c.Start+c.Length], c.Data) {
|
||||||
|
t.Fatalf("invalid data for chunk returned: expected %02x, got %02x",
|
||||||
|
buf[c.Start:c.Start+c.Length], c.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup nullbyte data source
|
||||||
|
buf = bytes.Repeat([]byte{0}, len(chunks2)*MinSize)
|
||||||
|
ch = New(bytes.NewReader(buf), testPol)
|
||||||
|
|
||||||
|
testWithData(t, ch, chunks2, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkChunker(b *testing.B, checkDigest bool) {
|
||||||
|
size := 32 * 1024 * 1024
|
||||||
|
rd := bytes.NewReader(getRandom(23, size))
|
||||||
|
ch := New(rd, testPol)
|
||||||
|
buf := make([]byte, MaxSize)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.SetBytes(int64(size))
|
||||||
|
|
||||||
|
var chunks int
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
chunks = 0
|
||||||
|
|
||||||
|
_, err := rd.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Seek() return error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.Reset(rd, testPol)
|
||||||
|
|
||||||
|
cur := 0
|
||||||
|
for {
|
||||||
|
chunk, err := ch.Next(buf)
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Unexpected error occurred: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chunk.Length != chunks1[cur].Length {
|
||||||
|
b.Errorf("wrong chunk length, want %d, got %d",
|
||||||
|
chunks1[cur].Length, chunk.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chunk.Cut != chunks1[cur].CutFP {
|
||||||
|
b.Errorf("wrong cut fingerprint, want 0x%x, got 0x%x",
|
||||||
|
chunks1[cur].CutFP, chunk.Cut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkDigest {
|
||||||
|
h := hashData(chunk.Data)
|
||||||
|
if !bytes.Equal(h, chunks1[cur].Digest) {
|
||||||
|
b.Errorf("wrong digest, want %x, got %x",
|
||||||
|
chunks1[cur].Digest, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks++
|
||||||
|
cur++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Logf("%d chunks, average chunk size: %d bytes", chunks, size/chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkChunkerWithSHA256(b *testing.B) {
|
||||||
|
benchmarkChunker(b, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkChunker(b *testing.B) {
|
||||||
|
benchmarkChunker(b, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewChunker(b *testing.B) {
|
||||||
|
p, err := RandomPolynomial()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
New(bytes.NewBuffer(nil), p)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2014 Alexander Neumann. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package chunker implements Content Defined Chunking (CDC) based on a rolling
|
||||||
|
Rabin Checksum.
|
||||||
|
|
||||||
|
Choosing a Random Irreducible Polynomial
|
||||||
|
|
||||||
|
The function RandomPolynomial() returns a new random polynomial of degree 53
|
||||||
|
for use with the chunker. The degree 53 is chosen because it is the largest
|
||||||
|
prime below 64-8 = 56, so that the top 8 bits of an uint64 can be used for
|
||||||
|
optimising calculations in the chunker.
|
||||||
|
|
||||||
|
A random polynomial is chosen selecting 64 random bits, masking away bits
|
||||||
|
64..54 and setting bit 53 to one (otherwise the polynomial is not of the
|
||||||
|
desired degree) and bit 0 to one (otherwise the polynomial is trivially
|
||||||
|
reducible), so that 51 bits are chosen at random.
|
||||||
|
|
||||||
|
This process is repeated until Irreducible() returns true, then this
|
||||||
|
polynomials is returned. If this doesn't happen after 1 million tries, the
|
||||||
|
function returns an error. The probability for selecting an irreducible
|
||||||
|
polynomial at random is about 7.5% ( (2^53-2)/53 / 2^51), so the probability
|
||||||
|
that no irreducible polynomial has been found after 100 tries is lower than
|
||||||
|
0.04%.
|
||||||
|
|
||||||
|
Verifying Irreducible Polynomials
|
||||||
|
|
||||||
|
During development the results have been verified using the computational
|
||||||
|
discrete algebra system GAP, which can be obtained from the website at
|
||||||
|
http://www.gap-system.org/.
|
||||||
|
|
||||||
|
For filtering a given list of polynomials in hexadecimal coefficient notation,
|
||||||
|
the following script can be used:
|
||||||
|
|
||||||
|
# create x over F_2 = GF(2)
|
||||||
|
x := Indeterminate(GF(2), "x");
|
||||||
|
|
||||||
|
# test if polynomial is irreducible, i.e. the number of factors is one
|
||||||
|
IrredPoly := function (poly)
|
||||||
|
return (Length(Factors(poly)) = 1);
|
||||||
|
end;;
|
||||||
|
|
||||||
|
# create a polynomial in x from the hexadecimal representation of the
|
||||||
|
# coefficients
|
||||||
|
Hex2Poly := function (s)
|
||||||
|
return ValuePol(CoefficientsQadic(IntHexString(s), 2), x);
|
||||||
|
end;;
|
||||||
|
|
||||||
|
# list of candidates, in hex
|
||||||
|
candidates := [ "3DA3358B4DC173" ];
|
||||||
|
|
||||||
|
# create real polynomials
|
||||||
|
L := List(candidates, Hex2Poly);
|
||||||
|
|
||||||
|
# filter and display the list of irreducible polynomials contained in L
|
||||||
|
Display(Filtered(L, x -> (IrredPoly(x))));
|
||||||
|
|
||||||
|
All irreducible polynomials from the list are written to the output.
|
||||||
|
|
||||||
|
Background Literature
|
||||||
|
|
||||||
|
An introduction to Rabin Fingerprints/Checksums can be found in the following articles:
|
||||||
|
|
||||||
|
Michael O. Rabin (1981): "Fingerprinting by Random Polynomials"
|
||||||
|
http://www.xmailserver.org/rabin.pdf
|
||||||
|
|
||||||
|
Ross N. Williams (1993): "A Painless Guide to CRC Error Detection Algorithms"
|
||||||
|
http://www.zlib.net/crc_v3.txt
|
||||||
|
|
||||||
|
Andrei Z. Broder (1993): "Some Applications of Rabin's Fingerprinting Method"
|
||||||
|
http://www.xmailserver.org/rabin_apps.pdf
|
||||||
|
|
||||||
|
Shuhong Gao and Daniel Panario (1997): "Tests and Constructions of Irreducible Polynomials over Finite Fields"
|
||||||
|
http://www.math.clemson.edu/~sgao/papers/GP97a.pdf
|
||||||
|
|
||||||
|
Andrew Kadatch, Bob Jenkins (2007): "Everything we know about CRC but afraid to forget"
|
||||||
|
http://crcutil.googlecode.com/files/crc-doc.1.0.pdf
|
||||||
|
|
||||||
|
*/
|
||||||
|
package chunker
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package chunker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleChunker() {
|
||||||
|
// generate 32MiB of deterministic pseudo-random data
|
||||||
|
data := getRandom(23, 32*1024*1024)
|
||||||
|
|
||||||
|
// create a chunker
|
||||||
|
chunker := New(bytes.NewReader(data), Pol(0x3DA3358B4DC173))
|
||||||
|
|
||||||
|
// reuse this buffer
|
||||||
|
buf := make([]byte, 8*1024*1024)
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
chunk, err := chunker.Next(buf)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%d %02x\n", chunk.Length, sha256.Sum256(chunk.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// 2163460 4b94cb2cf293855ea43bf766731c74969b91aa6bf3c078719aabdd19860d590d
|
||||||
|
// 643703 5727a63c0964f365ab8ed2ccf604912f2ea7be29759a2b53ede4d6841e397407
|
||||||
|
// 1528956 a73759636a1e7a2758767791c69e81b69fb49236c6929e5d1b654e06e37674ba
|
||||||
|
// 1955808 c955fb059409b25f07e5ae09defbbc2aadf117c97a3724e06ad4abd2787e6824
|
||||||
|
// 2222372 6ba5e9f7e1b310722be3627716cf469be941f7f3e39a4c3bcefea492ec31ee56
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/restic/chunker
|
||||||
|
|
||||||
|
go 1.14
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
package chunker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pol is a polynomial from F_2[X].
|
||||||
|
type Pol uint64
|
||||||
|
|
||||||
|
// Add returns x+y.
|
||||||
|
func (x Pol) Add(y Pol) Pol {
|
||||||
|
r := Pol(uint64(x) ^ uint64(y))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// mulOverflows returns true if the multiplication would overflow uint64.
|
||||||
|
// Code by Rob Pike, see
|
||||||
|
// https://groups.google.com/d/msg/golang-nuts/h5oSN5t3Au4/KaNQREhZh0QJ
|
||||||
|
func mulOverflows(a, b Pol) bool {
|
||||||
|
if a <= 1 || b <= 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c := a.mul(b)
|
||||||
|
d := c.Div(b)
|
||||||
|
if d != a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Pol) mul(y Pol) Pol {
|
||||||
|
if x == 0 || y == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var res Pol
|
||||||
|
for i := 0; i <= y.Deg(); i++ {
|
||||||
|
if (y & (1 << uint(i))) > 0 {
|
||||||
|
res = res.Add(x << uint(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mul returns x*y. When an overflow occurs, Mul panics.
|
||||||
|
func (x Pol) Mul(y Pol) Pol {
|
||||||
|
if mulOverflows(x, y) {
|
||||||
|
panic("multiplication would overflow uint64")
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.mul(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deg returns the degree of the polynomial x. If x is zero, -1 is returned.
|
||||||
|
func (x Pol) Deg() int {
|
||||||
|
// the degree of 0 is -1
|
||||||
|
if x == 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// see https://graphics.stanford.edu/~seander/bithacks.html#IntegerLog
|
||||||
|
|
||||||
|
r := 0
|
||||||
|
if uint64(x)&0xffffffff00000000 > 0 {
|
||||||
|
x >>= 32
|
||||||
|
r |= 32
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint64(x)&0xffff0000 > 0 {
|
||||||
|
x >>= 16
|
||||||
|
r |= 16
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint64(x)&0xff00 > 0 {
|
||||||
|
x >>= 8
|
||||||
|
r |= 8
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint64(x)&0xf0 > 0 {
|
||||||
|
x >>= 4
|
||||||
|
r |= 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint64(x)&0xc > 0 {
|
||||||
|
x >>= 2
|
||||||
|
r |= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint64(x)&0x2 > 0 {
|
||||||
|
r |= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the coefficients in hex.
|
||||||
|
func (x Pol) String() string {
|
||||||
|
return "0x" + strconv.FormatUint(uint64(x), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand returns the string representation of the polynomial x.
|
||||||
|
func (x Pol) Expand() string {
|
||||||
|
if x == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
s := ""
|
||||||
|
for i := x.Deg(); i > 1; i-- {
|
||||||
|
if x&(1<<uint(i)) > 0 {
|
||||||
|
s += fmt.Sprintf("+x^%d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if x&2 > 0 {
|
||||||
|
s += "+x"
|
||||||
|
}
|
||||||
|
|
||||||
|
if x&1 > 0 {
|
||||||
|
s += "+1"
|
||||||
|
}
|
||||||
|
|
||||||
|
return s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DivMod returns x / d = q, and remainder r,
|
||||||
|
// see https://en.wikipedia.org/wiki/Division_algorithm
|
||||||
|
func (x Pol) DivMod(d Pol) (Pol, Pol) {
|
||||||
|
if x == 0 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if d == 0 {
|
||||||
|
panic("division by zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
D := d.Deg()
|
||||||
|
diff := x.Deg() - D
|
||||||
|
if diff < 0 {
|
||||||
|
return 0, x
|
||||||
|
}
|
||||||
|
|
||||||
|
var q Pol
|
||||||
|
for diff >= 0 {
|
||||||
|
m := d << uint(diff)
|
||||||
|
q |= (1 << uint(diff))
|
||||||
|
x = x.Add(m)
|
||||||
|
|
||||||
|
diff = x.Deg() - D
|
||||||
|
}
|
||||||
|
|
||||||
|
return q, x
|
||||||
|
}
|
||||||
|
|
||||||
|
// Div returns the integer division result x / d.
|
||||||
|
func (x Pol) Div(d Pol) Pol {
|
||||||
|
q, _ := x.DivMod(d)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mod returns the remainder of x / d
|
||||||
|
func (x Pol) Mod(d Pol) Pol {
|
||||||
|
_, r := x.DivMod(d)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// I really dislike having a function that does not terminate, so specify a
|
||||||
|
// really large upper bound for finding a new irreducible polynomial, and
|
||||||
|
// return an error when no irreducible polynomial has been found within
|
||||||
|
// randPolMaxTries.
|
||||||
|
const randPolMaxTries = 1e6
|
||||||
|
|
||||||
|
// RandomPolynomial returns a new random irreducible polynomial
|
||||||
|
// of degree 53 using the default System CSPRNG as source.
|
||||||
|
// It is equivalent to calling DerivePolynomial(rand.Reader).
|
||||||
|
func RandomPolynomial() (Pol, error) {
|
||||||
|
return DerivePolynomial(rand.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DerivePolynomial returns an irreducible polynomial of degree 53
|
||||||
|
// (largest prime number below 64-8) by reading bytes from source.
|
||||||
|
// There are (2^53-2/53) irreducible polynomials of degree 53 in
|
||||||
|
// F_2[X], c.f. Michael O. Rabin (1981): "Fingerprinting by Random
|
||||||
|
// Polynomials", page 4. If no polynomial could be found in one
|
||||||
|
// million tries, an error is returned.
|
||||||
|
func DerivePolynomial(source io.Reader) (Pol, error) {
|
||||||
|
for i := 0; i < randPolMaxTries; i++ {
|
||||||
|
var f Pol
|
||||||
|
|
||||||
|
// choose polynomial at (pseudo)random
|
||||||
|
err := binary.Read(source, binary.LittleEndian, &f)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// mask away bits above bit 53
|
||||||
|
f &= Pol((1 << 54) - 1)
|
||||||
|
|
||||||
|
// set highest and lowest bit so that the degree is 53 and the
|
||||||
|
// polynomial is not trivially reducible
|
||||||
|
f |= (1 << 53) | 1
|
||||||
|
|
||||||
|
// test if f is irreducible
|
||||||
|
if f.Irreducible() {
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is reached, we haven't found an irreducible polynomial in
|
||||||
|
// randPolMaxTries. This error is very unlikely to occur.
|
||||||
|
return 0, errors.New("unable to find new random irreducible polynomial")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GCD computes the Greatest Common Divisor x and f.
|
||||||
|
func (x Pol) GCD(f Pol) Pol {
|
||||||
|
if f == 0 {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
if x == 0 {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
if x.Deg() < f.Deg() {
|
||||||
|
x, f = f, x
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.GCD(x.Mod(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Irreducible returns true iff x is irreducible over F_2. This function
|
||||||
|
// uses Ben Or's reducibility test.
|
||||||
|
//
|
||||||
|
// For details see "Tests and Constructions of Irreducible Polynomials over
|
||||||
|
// Finite Fields".
|
||||||
|
func (x Pol) Irreducible() bool {
|
||||||
|
for i := 1; i <= x.Deg()/2; i++ {
|
||||||
|
if x.GCD(qp(uint(i), x)) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MulMod computes x*f mod g
|
||||||
|
func (x Pol) MulMod(f, g Pol) Pol {
|
||||||
|
if x == 0 || f == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var res Pol
|
||||||
|
for i := 0; i <= f.Deg(); i++ {
|
||||||
|
if (f & (1 << uint(i))) > 0 {
|
||||||
|
a := x
|
||||||
|
for j := 0; j < i; j++ {
|
||||||
|
a = a.Mul(2).Mod(g)
|
||||||
|
}
|
||||||
|
res = res.Add(a).Mod(g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// qp computes the polynomial (x^(2^p)-x) mod g. This is needed for the
|
||||||
|
// reducibility test.
|
||||||
|
func qp(p uint, g Pol) Pol {
|
||||||
|
num := (1 << p)
|
||||||
|
i := 1
|
||||||
|
|
||||||
|
// start with x
|
||||||
|
res := Pol(2)
|
||||||
|
|
||||||
|
for i < num {
|
||||||
|
// repeatedly square res
|
||||||
|
res = res.MulMod(res, g)
|
||||||
|
i *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// add x
|
||||||
|
return res.Add(2).Mod(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON returns the JSON representation of the Pol.
|
||||||
|
func (x Pol) MarshalJSON() ([]byte, error) {
|
||||||
|
buf := strconv.AppendUint([]byte{'"'}, uint64(x), 16)
|
||||||
|
buf = append(buf, '"')
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON parses a Pol from the JSON data.
|
||||||
|
func (x *Pol) UnmarshalJSON(data []byte) error {
|
||||||
|
if len(data) < 2 {
|
||||||
|
return errors.New("invalid string for polynomial")
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseUint(string(data[1:len(data)-1]), 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = Pol(n)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
package chunker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var polAddTests = []struct {
|
||||||
|
x, y Pol
|
||||||
|
sum Pol
|
||||||
|
}{
|
||||||
|
{23, 16, 23 ^ 16},
|
||||||
|
{0x9a7e30d1e855e0a0, 0x670102a1f4bcd414, 0xfd7f32701ce934b4},
|
||||||
|
{0x9a7e30d1e855e0a0, 0x9a7e30d1e855e0a0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolAdd(t *testing.T) {
|
||||||
|
for i, test := range polAddTests {
|
||||||
|
if test.sum != test.x.Add(test.y) {
|
||||||
|
t.Errorf("test %d failed: sum != x+y", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.sum != test.y.Add(test.x) {
|
||||||
|
t.Errorf("test %d failed: sum != y+x", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBin(s string) Pol {
|
||||||
|
i, err := strconv.ParseUint(s, 2, 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pol(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
var polMulTests = []struct {
|
||||||
|
x, y Pol
|
||||||
|
res Pol
|
||||||
|
}{
|
||||||
|
{1, 2, 2},
|
||||||
|
{
|
||||||
|
parseBin("1101"),
|
||||||
|
parseBin("10"),
|
||||||
|
parseBin("11010"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("1101"),
|
||||||
|
parseBin("11"),
|
||||||
|
parseBin("10111"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0x40000000,
|
||||||
|
0x40000000,
|
||||||
|
0x1000000000000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("1010"),
|
||||||
|
parseBin("100100"),
|
||||||
|
parseBin("101101000"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("100"),
|
||||||
|
parseBin("11"),
|
||||||
|
parseBin("1100"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("11"),
|
||||||
|
parseBin("110101"),
|
||||||
|
parseBin("1011111"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("10011"),
|
||||||
|
parseBin("110101"),
|
||||||
|
parseBin("1100001111"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolMul(t *testing.T) {
|
||||||
|
for i, test := range polMulTests {
|
||||||
|
m := test.x.Mul(test.y)
|
||||||
|
if test.res != m {
|
||||||
|
t.Errorf("TestPolMul failed for test %d: %v * %v: want %v, got %v",
|
||||||
|
i, test.x, test.y, test.res, m)
|
||||||
|
}
|
||||||
|
m = test.y.Mul(test.x)
|
||||||
|
if test.res != test.y.Mul(test.x) {
|
||||||
|
t.Errorf("TestPolMul failed for %d: %v * %v: want %v, got %v",
|
||||||
|
i, test.x, test.y, test.res, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolMulOverflow(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
// try to recover overflow error
|
||||||
|
err := recover()
|
||||||
|
|
||||||
|
if e, ok := err.(string); ok && e == "multiplication would overflow uint64" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("invalid error raised: %v", err)
|
||||||
|
// re-raise error if not overflow
|
||||||
|
panic(err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
x := Pol(1 << 63)
|
||||||
|
x.Mul(2)
|
||||||
|
t.Fatal("overflow test did not panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
var polDivTests = []struct {
|
||||||
|
x, y Pol
|
||||||
|
res Pol
|
||||||
|
}{
|
||||||
|
{10, 50, 0},
|
||||||
|
{0, 1, 0},
|
||||||
|
{
|
||||||
|
parseBin("101101000"), // 0x168
|
||||||
|
parseBin("1010"), // 0xa
|
||||||
|
parseBin("100100"), // 0x24
|
||||||
|
},
|
||||||
|
{2, 2, 1},
|
||||||
|
{
|
||||||
|
0x8000000000000000,
|
||||||
|
0x8000000000000000,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("1100"),
|
||||||
|
parseBin("100"),
|
||||||
|
parseBin("11"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("1100001111"),
|
||||||
|
parseBin("10011"),
|
||||||
|
parseBin("110101"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolDiv(t *testing.T) {
|
||||||
|
for i, test := range polDivTests {
|
||||||
|
m := test.x.Div(test.y)
|
||||||
|
if test.res != m {
|
||||||
|
t.Errorf("TestPolDiv failed for test %d: %v * %v: want %v, got %v",
|
||||||
|
i, test.x, test.y, test.res, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolDeg(t *testing.T) {
|
||||||
|
var x Pol
|
||||||
|
if x.Deg() != -1 {
|
||||||
|
t.Errorf("deg(0) is not -1: %v", x.Deg())
|
||||||
|
}
|
||||||
|
|
||||||
|
x = 1
|
||||||
|
if x.Deg() != 0 {
|
||||||
|
t.Errorf("deg(1) is not 0: %v", x.Deg())
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 64; i++ {
|
||||||
|
x = 1 << uint(i)
|
||||||
|
if x.Deg() != i {
|
||||||
|
t.Errorf("deg(1<<%d) is not %d: %v", i, i, x.Deg())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var polModTests = []struct {
|
||||||
|
x, y Pol
|
||||||
|
res Pol
|
||||||
|
}{
|
||||||
|
{10, 50, 10},
|
||||||
|
{0, 1, 0},
|
||||||
|
{
|
||||||
|
parseBin("101101001"),
|
||||||
|
parseBin("1010"),
|
||||||
|
parseBin("1"),
|
||||||
|
},
|
||||||
|
{2, 2, 0},
|
||||||
|
{
|
||||||
|
0x8000000000000000,
|
||||||
|
0x8000000000000000,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("1100"),
|
||||||
|
parseBin("100"),
|
||||||
|
parseBin("0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("1100001111"),
|
||||||
|
parseBin("10011"),
|
||||||
|
parseBin("0"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolModt(t *testing.T) {
|
||||||
|
for i, test := range polModTests {
|
||||||
|
res := test.x.Mod(test.y)
|
||||||
|
if test.res != res {
|
||||||
|
t.Errorf("test %d failed: want %v, got %v", i, test.res, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPolDivMod(t *testing.B) {
|
||||||
|
f := Pol(0x2482734cacca49)
|
||||||
|
g := Pol(0x3af4b284899)
|
||||||
|
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
g.DivMod(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPolDiv(t *testing.B) {
|
||||||
|
f := Pol(0x2482734cacca49)
|
||||||
|
g := Pol(0x3af4b284899)
|
||||||
|
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
g.Div(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPolMod(t *testing.B) {
|
||||||
|
f := Pol(0x2482734cacca49)
|
||||||
|
g := Pol(0x3af4b284899)
|
||||||
|
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
g.Mod(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPolDeg(t *testing.B) {
|
||||||
|
f := Pol(0x3af4b284899)
|
||||||
|
d := f.Deg()
|
||||||
|
if d != 41 {
|
||||||
|
t.Fatalf("BenchmalPolDeg: Wrong degree %d returned, expected %d",
|
||||||
|
d, 41)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
f.Deg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRandomPolynomial(t *testing.T) {
|
||||||
|
_, err := RandomPolynomial()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRandomPolynomial(t *testing.B) {
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
_, err := RandomPolynomial()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandPolynomial(t *testing.T) {
|
||||||
|
pol := Pol(0x3DA3358B4DC173)
|
||||||
|
s := pol.Expand()
|
||||||
|
if s != "x^53+x^52+x^51+x^50+x^48+x^47+x^45+x^41+x^40+x^37+x^36+x^34+x^32+x^31+x^27+x^25+x^24+x^22+x^19+x^18+x^16+x^15+x^14+x^8+x^6+x^5+x^4+x+1" {
|
||||||
|
t.Fatal("wrong result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var polIrredTests = []struct {
|
||||||
|
f Pol
|
||||||
|
irred bool
|
||||||
|
}{
|
||||||
|
{0x38f1e565e288df, false},
|
||||||
|
{0x3DA3358B4DC173, true},
|
||||||
|
{0x30a8295b9d5c91, false},
|
||||||
|
{0x255f4350b962cb, false},
|
||||||
|
{0x267f776110a235, false},
|
||||||
|
{0x2f4dae10d41227, false},
|
||||||
|
{0x2482734cacca49, true},
|
||||||
|
{0x312daf4b284899, false},
|
||||||
|
{0x29dfb6553d01d1, false},
|
||||||
|
{0x3548245eb26257, false},
|
||||||
|
{0x3199e7ef4211b3, false},
|
||||||
|
{0x362f39017dae8b, false},
|
||||||
|
{0x200d57aa6fdacb, false},
|
||||||
|
{0x35e0a4efa1d275, false},
|
||||||
|
{0x2ced55b026577f, false},
|
||||||
|
{0x260b012010893d, false},
|
||||||
|
{0x2df29cbcd59e9d, false},
|
||||||
|
{0x3f2ac7488bd429, false},
|
||||||
|
{0x3e5cb1711669fb, false},
|
||||||
|
{0x226d8de57a9959, false},
|
||||||
|
{0x3c8de80aaf5835, false},
|
||||||
|
{0x2026a59efb219b, false},
|
||||||
|
{0x39dfa4d13fb231, false},
|
||||||
|
{0x3143d0464b3299, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolIrreducible(t *testing.T) {
|
||||||
|
for _, test := range polIrredTests {
|
||||||
|
if test.f.Irreducible() != test.irred {
|
||||||
|
t.Errorf("Irreducibility test for Polynomial %v failed: got %v, wanted %v",
|
||||||
|
test.f, test.f.Irreducible(), test.irred)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPolIrreducible(b *testing.B) {
|
||||||
|
// find first irreducible polynomial
|
||||||
|
var pol Pol
|
||||||
|
for _, test := range polIrredTests {
|
||||||
|
if test.irred {
|
||||||
|
pol = test.f
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if !pol.Irreducible() {
|
||||||
|
b.Errorf("Irreducibility test for Polynomial %v failed", pol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var polGCDTests = []struct {
|
||||||
|
f1 Pol
|
||||||
|
f2 Pol
|
||||||
|
gcd Pol
|
||||||
|
}{
|
||||||
|
{10, 50, 2},
|
||||||
|
{0, 1, 1},
|
||||||
|
{
|
||||||
|
parseBin("101101001"),
|
||||||
|
parseBin("1010"),
|
||||||
|
parseBin("1"),
|
||||||
|
},
|
||||||
|
{2, 2, 2},
|
||||||
|
{
|
||||||
|
parseBin("1010"),
|
||||||
|
parseBin("11"),
|
||||||
|
parseBin("11"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0x8000000000000000,
|
||||||
|
0x8000000000000000,
|
||||||
|
0x8000000000000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("1100"),
|
||||||
|
parseBin("101"),
|
||||||
|
parseBin("11"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parseBin("1100001111"),
|
||||||
|
parseBin("10011"),
|
||||||
|
parseBin("10011"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0x3DA3358B4DC173,
|
||||||
|
0x3DA3358B4DC173,
|
||||||
|
0x3DA3358B4DC173,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0x3DA3358B4DC173,
|
||||||
|
0x230d2259defd,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0x230d2259defd,
|
||||||
|
0x51b492b3eff2,
|
||||||
|
parseBin("10011"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolGCD(t *testing.T) {
|
||||||
|
for i, test := range polGCDTests {
|
||||||
|
gcd := test.f1.GCD(test.f2)
|
||||||
|
if test.gcd != gcd {
|
||||||
|
t.Errorf("GCD test %d (%+v) failed: got %v, wanted %v",
|
||||||
|
i, test, gcd, test.gcd)
|
||||||
|
}
|
||||||
|
|
||||||
|
gcd = test.f2.GCD(test.f1)
|
||||||
|
if test.gcd != gcd {
|
||||||
|
t.Errorf("GCD test %d (%+v) failed: got %v, wanted %v",
|
||||||
|
i, test, gcd, test.gcd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var polMulModTests = []struct {
|
||||||
|
f1 Pol
|
||||||
|
f2 Pol
|
||||||
|
g Pol
|
||||||
|
mod Pol
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
0x1230,
|
||||||
|
0x230,
|
||||||
|
0x55,
|
||||||
|
0x22,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0x0eae8c07dbbb3026,
|
||||||
|
0xd5d6db9de04771de,
|
||||||
|
0xdd2bda3b77c9,
|
||||||
|
0x425ae8595b7a,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolMulMod(t *testing.T) {
|
||||||
|
for i, test := range polMulModTests {
|
||||||
|
mod := test.f1.MulMod(test.f2, test.g)
|
||||||
|
if mod != test.mod {
|
||||||
|
t.Errorf("MulMod test %d (%+v) failed: got %v, wanted %v",
|
||||||
|
i, test, mod, test.mod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dump
|
package main
|
||||||
|
|
||||||
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
|
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
|
||||||
|
|
||||||
@@ -35,6 +35,17 @@ type aclElement struct {
|
|||||||
Perm uint16
|
Perm uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *aclSID) setUID(uid uint32) {
|
||||||
|
*a = aclSID(uid) | (aclUser << 32)
|
||||||
|
}
|
||||||
|
func (a *aclSID) setGID(gid uint32) {
|
||||||
|
*a = aclSID(gid) | (aclGroup << 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *aclSID) setType(tp int) {
|
||||||
|
*a = aclSID(tp) << 32
|
||||||
|
}
|
||||||
|
|
||||||
func (a aclSID) getType() int {
|
func (a aclSID) getType() int {
|
||||||
return int(a >> 32)
|
return int(a >> 32)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dump
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -16,6 +17,8 @@ var cleanupHandlers struct {
|
|||||||
ch chan os.Signal
|
ch chan os.Signal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var stderr = os.Stderr
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cleanupHandlers.ch = make(chan os.Signal, 1)
|
cleanupHandlers.ch = make(chan os.Signal, 1)
|
||||||
go CleanupHandler(cleanupHandlers.ch)
|
go CleanupHandler(cleanupHandlers.ch)
|
||||||
@@ -48,7 +51,7 @@ func RunCleanupHandlers() {
|
|||||||
for _, f := range cleanupHandlers.list {
|
for _, f := range cleanupHandlers.list {
|
||||||
err := f()
|
err := f()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error in cleanup handler: %v\n", err)
|
fmt.Fprintf(stderr, "error in cleanup handler: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cleanupHandlers.list = nil
|
cleanupHandlers.list = nil
|
||||||
@@ -58,7 +61,7 @@ func RunCleanupHandlers() {
|
|||||||
func CleanupHandler(c <-chan os.Signal) {
|
func CleanupHandler(c <-chan os.Signal) {
|
||||||
for s := range c {
|
for s := range c {
|
||||||
debug.Log("signal %v received, cleaning up", s)
|
debug.Log("signal %v received, cleaning up", s)
|
||||||
Warnf("%ssignal %v received, cleaning up\n", ClearLine(), s)
|
fmt.Fprintf(stderr, "%ssignal %v received, cleaning up\n", ClearLine(), s)
|
||||||
|
|
||||||
code := 0
|
code := 0
|
||||||
|
|
||||||
|
|||||||
+41
-52
@@ -39,9 +39,10 @@ given as the arguments.
|
|||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
===========
|
===========
|
||||||
|
|
||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||||
Exit status is 1 if there was a fatal error (no snapshot created).
|
|
||||||
Exit status is 3 if some source data could not be read (incomplete snapshot created).
|
Note that some issues such as unreadable or deleted files during backup
|
||||||
|
currently doesn't result in a non-zero error exit status.
|
||||||
`,
|
`,
|
||||||
PreRun: func(cmd *cobra.Command, args []string) {
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
if backupOptions.Host == "" {
|
if backupOptions.Host == "" {
|
||||||
@@ -78,31 +79,26 @@ Exit status is 3 if some source data could not be read (incomplete snapshot crea
|
|||||||
|
|
||||||
// BackupOptions bundles all options for the backup command.
|
// BackupOptions bundles all options for the backup command.
|
||||||
type BackupOptions struct {
|
type BackupOptions struct {
|
||||||
Parent string
|
Parent string
|
||||||
Force bool
|
Force bool
|
||||||
Excludes []string
|
Excludes []string
|
||||||
InsensitiveExcludes []string
|
InsensitiveExcludes []string
|
||||||
ExcludeFiles []string
|
ExcludeFiles []string
|
||||||
InsensitiveExcludeFiles []string
|
ExcludeOtherFS bool
|
||||||
ExcludeOtherFS bool
|
ExcludeIfPresent []string
|
||||||
ExcludeIfPresent []string
|
ExcludeCaches bool
|
||||||
ExcludeCaches bool
|
Stdin bool
|
||||||
ExcludeLargerThan string
|
StdinFilename string
|
||||||
Stdin bool
|
Tags []string
|
||||||
StdinFilename string
|
Host string
|
||||||
Tags []string
|
FilesFrom []string
|
||||||
Host string
|
TimeStamp string
|
||||||
FilesFrom []string
|
WithAtime bool
|
||||||
TimeStamp string
|
IgnoreInode bool
|
||||||
WithAtime bool
|
|
||||||
IgnoreInode bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupOptions BackupOptions
|
var backupOptions BackupOptions
|
||||||
|
|
||||||
// ErrInvalidSourceData is used to report an incomplete backup
|
|
||||||
var ErrInvalidSourceData = errors.New("failed to read all source data during backup")
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmdRoot.AddCommand(cmdBackup)
|
cmdRoot.AddCommand(cmdBackup)
|
||||||
|
|
||||||
@@ -112,11 +108,9 @@ func init() {
|
|||||||
f.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
|
f.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
|
||||||
f.StringArrayVar(&backupOptions.InsensitiveExcludes, "iexclude", nil, "same as --exclude `pattern` but ignores the casing of filenames")
|
f.StringArrayVar(&backupOptions.InsensitiveExcludes, "iexclude", nil, "same as --exclude `pattern` but ignores the casing of filenames")
|
||||||
f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
|
f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
|
||||||
f.StringArrayVar(&backupOptions.InsensitiveExcludeFiles, "iexclude-file", nil, "same as --exclude-file but ignores casing of `file`names in patterns")
|
|
||||||
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
|
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
|
||||||
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes `filename[:header]`, exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes `filename[:header]`, exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
||||||
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
|
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See http://bford.info/cachedir/spec.html for the Cache Directory Tagging Standard`)
|
||||||
f.StringVar(&backupOptions.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
|
|
||||||
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
|
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
|
||||||
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
|
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
|
||||||
f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
|
f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
|
||||||
@@ -243,14 +237,6 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, t
|
|||||||
opts.Excludes = append(opts.Excludes, excludes...)
|
opts.Excludes = append(opts.Excludes, excludes...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opts.InsensitiveExcludeFiles) > 0 {
|
|
||||||
excludes, err := readExcludePatternsFromFiles(opts.InsensitiveExcludeFiles)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
opts.InsensitiveExcludes = append(opts.InsensitiveExcludes, excludes...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(opts.InsensitiveExcludes) > 0 {
|
if len(opts.InsensitiveExcludes) > 0 {
|
||||||
fs = append(fs, rejectByInsensitivePattern(opts.InsensitiveExcludes))
|
fs = append(fs, rejectByInsensitivePattern(opts.InsensitiveExcludes))
|
||||||
}
|
}
|
||||||
@@ -287,14 +273,6 @@ func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets
|
|||||||
fs = append(fs, f)
|
fs = append(fs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opts.ExcludeLargerThan) != 0 && !opts.Stdin {
|
|
||||||
f, err := rejectBySize(opts.ExcludeLargerThan)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fs = append(fs, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs, nil
|
return fs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +415,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
|||||||
var t tomb.Tomb
|
var t tomb.Tomb
|
||||||
|
|
||||||
if gopts.verbosity >= 2 && !gopts.JSON {
|
if gopts.verbosity >= 2 && !gopts.JSON {
|
||||||
Verbosef("open repository\n")
|
term.Print("open repository\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := OpenRepository(gopts)
|
repo, err := OpenRepository(gopts)
|
||||||
@@ -579,11 +557,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
|||||||
arch.SelectByName = selectByNameFilter
|
arch.SelectByName = selectByNameFilter
|
||||||
arch.Select = selectFilter
|
arch.Select = selectFilter
|
||||||
arch.WithAtime = opts.WithAtime
|
arch.WithAtime = opts.WithAtime
|
||||||
success := true
|
arch.Error = p.Error
|
||||||
arch.Error = func(item string, fi os.FileInfo, err error) error {
|
|
||||||
success = false
|
|
||||||
return p.Error(item, fi, err)
|
|
||||||
}
|
|
||||||
arch.CompleteItem = p.CompleteItem
|
arch.CompleteItem = p.CompleteItem
|
||||||
arch.StartFile = p.StartFile
|
arch.StartFile = p.StartFile
|
||||||
arch.CompleteBlob = p.CompleteBlob
|
arch.CompleteBlob = p.CompleteBlob
|
||||||
@@ -601,6 +575,24 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
|||||||
ParentSnapshot: *parentSnapshotID,
|
ParentSnapshot: *parentSnapshotID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploader := archiver.IndexUploader{
|
||||||
|
Repository: repo,
|
||||||
|
Start: func() {
|
||||||
|
if !gopts.JSON {
|
||||||
|
p.VV("uploading intermediate index")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Complete: func(id restic.ID) {
|
||||||
|
if !gopts.JSON {
|
||||||
|
p.V("uploaded intermediate index %v", id.Str())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Go(func() error {
|
||||||
|
return uploader.Upload(gopts.ctx, t.Context(gopts.ctx), 30*time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
p.V("start backup on %v", targets)
|
p.V("start backup on %v", targets)
|
||||||
}
|
}
|
||||||
@@ -620,9 +612,6 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
|||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
p.P("snapshot %s saved\n", id.Str())
|
p.P("snapshot %s saved\n", id.Str())
|
||||||
}
|
}
|
||||||
if !success {
|
|
||||||
return ErrInvalidSourceData
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return error if any
|
// Return error if any
|
||||||
return err
|
return err
|
||||||
|
|||||||
+18
-12
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
fmt.Println(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "index":
|
case "index":
|
||||||
buf, err := repo.LoadAndDecrypt(gopts.ctx, nil, restic.IndexFile, id)
|
buf, err := repo.LoadAndDecrypt(gopts.ctx, nil, restic.IndexFile, id)
|
||||||
@@ -82,8 +84,9 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
_, err = os.Stdout.Write(append(buf, '\n'))
|
||||||
return nil
|
return err
|
||||||
|
|
||||||
case "snapshot":
|
case "snapshot":
|
||||||
sn := &restic.Snapshot{}
|
sn := &restic.Snapshot{}
|
||||||
err = repo.LoadJSONUnpacked(gopts.ctx, restic.SnapshotFile, id, sn)
|
err = repo.LoadJSONUnpacked(gopts.ctx, restic.SnapshotFile, id, sn)
|
||||||
@@ -96,7 +99,8 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
fmt.Println(string(buf))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case "key":
|
case "key":
|
||||||
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
|
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
|
||||||
@@ -116,7 +120,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
fmt.Println(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "masterkey":
|
case "masterkey":
|
||||||
buf, err := json.MarshalIndent(repo.Key(), "", " ")
|
buf, err := json.MarshalIndent(repo.Key(), "", " ")
|
||||||
@@ -124,7 +128,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
fmt.Println(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "lock":
|
case "lock":
|
||||||
lock, err := restic.LoadLock(gopts.ctx, repo, id)
|
lock, err := restic.LoadLock(gopts.ctx, repo, id)
|
||||||
@@ -137,7 +141,8 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
fmt.Println(string(buf))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +154,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
switch tpe {
|
switch tpe {
|
||||||
case "pack":
|
case "pack":
|
||||||
h := restic.Handle{Type: restic.PackFile, Name: id.String()}
|
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
|
||||||
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
|
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -157,15 +162,16 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
hash := restic.Hash(buf)
|
hash := restic.Hash(buf)
|
||||||
if !hash.Equal(id) {
|
if !hash.Equal(id) {
|
||||||
Warnf("Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String())
|
fmt.Fprintf(stderr, "Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = globalOptions.stdout.Write(buf)
|
_, err = os.Stdout.Write(buf)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
case "blob":
|
case "blob":
|
||||||
for _, t := range []restic.BlobType{restic.DataBlob, restic.TreeBlob} {
|
for _, t := range []restic.BlobType{restic.DataBlob, restic.TreeBlob} {
|
||||||
if !repo.Index().Has(id, t) {
|
_, found := repo.Index().Lookup(id, t)
|
||||||
|
if !found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +180,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = globalOptions.stdout.Write(buf)
|
_, err = os.Stdout.Write(buf)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-7
@@ -3,8 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -98,6 +100,36 @@ func stringToIntSlice(param string) (split []uint, err error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
|
||||||
|
if gopts.Quiet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
readProgress := restic.NewProgress()
|
||||||
|
|
||||||
|
readProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||||
|
status := fmt.Sprintf("[%s] %s %d / %d items",
|
||||||
|
formatDuration(d),
|
||||||
|
formatPercent(s.Blobs, todo.Blobs),
|
||||||
|
s.Blobs, todo.Blobs)
|
||||||
|
|
||||||
|
if w := stdoutTerminalWidth(); w > 0 {
|
||||||
|
if len(status) > w {
|
||||||
|
max := w - len(status) - 4
|
||||||
|
status = status[:max] + "... "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintProgress("%s", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
readProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||||
|
fmt.Printf("\nduration: %s\n", formatDuration(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
return readProgress
|
||||||
|
}
|
||||||
|
|
||||||
// prepareCheckCache configures a special cache directory for check.
|
// prepareCheckCache configures a special cache directory for check.
|
||||||
//
|
//
|
||||||
// * if --with-cache is specified, the default cache is used
|
// * if --with-cache is specified, the default cache is used
|
||||||
@@ -203,7 +235,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
Warnf("%v\n", err)
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if orphanedPacks > 0 {
|
if orphanedPacks > 0 {
|
||||||
@@ -217,18 +249,18 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
|||||||
for err := range errChan {
|
for err := range errChan {
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
if e, ok := err.(checker.TreeError); ok {
|
if e, ok := err.(checker.TreeError); ok {
|
||||||
Warnf("error for tree %v:\n", e.ID.Str())
|
fmt.Fprintf(os.Stderr, "error for tree %v:\n", e.ID.Str())
|
||||||
for _, treeErr := range e.Errors {
|
for _, treeErr := range e.Errors {
|
||||||
Warnf(" %v\n", treeErr)
|
fmt.Fprintf(os.Stderr, " %v\n", treeErr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Warnf("error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.CheckUnused {
|
if opts.CheckUnused {
|
||||||
for _, id := range chkr.UnusedBlobs() {
|
for _, id := range chkr.UnusedBlobs() {
|
||||||
Verbosef("unused blob %v\n", id)
|
Verbosef("unused blob %v\n", id.Str())
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,14 +282,14 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
|||||||
Verbosef("read all data\n")
|
Verbosef("read all data\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
p := newProgressMax(!gopts.Quiet, packCount, "packs")
|
p := newReadProgress(gopts, restic.Stat{Blobs: packCount})
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
|
|
||||||
go chkr.ReadPacks(gopts.ctx, packs, p, errChan)
|
go chkr.ReadPacks(gopts.ctx, packs, p, errChan)
|
||||||
|
|
||||||
for err := range errChan {
|
for err := range errChan {
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
Warnf("%v\n", err)
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,233 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/restic/restic/internal/debug"
|
|
||||||
"github.com/restic/restic/internal/restic"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var cmdCopy = &cobra.Command{
|
|
||||||
Use: "copy [flags] [snapshotID ...]",
|
|
||||||
Short: "Copy snapshots from one repository to another",
|
|
||||||
Long: `
|
|
||||||
The "copy" command copies one or more snapshots from one repository to another
|
|
||||||
repository. Note that this will have to read (download) and write (upload) the
|
|
||||||
entire snapshot(s) due to the different encryption keys on the source and
|
|
||||||
destination, and that transferred files are not re-chunked, which may break
|
|
||||||
their deduplication. This can be mitigated by the "--copy-chunker-params"
|
|
||||||
option when initializing a new destination repository using the "init" command.
|
|
||||||
`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return runCopy(copyOptions, globalOptions, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyOptions bundles all options for the copy command.
|
|
||||||
type CopyOptions struct {
|
|
||||||
secondaryRepoOptions
|
|
||||||
Hosts []string
|
|
||||||
Tags restic.TagLists
|
|
||||||
Paths []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var copyOptions CopyOptions
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
cmdRoot.AddCommand(cmdCopy)
|
|
||||||
|
|
||||||
f := cmdCopy.Flags()
|
|
||||||
initSecondaryRepoOptions(f, ©Options.secondaryRepoOptions, "destination", "to copy snapshots to")
|
|
||||||
f.StringArrayVarP(©Options.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
|
||||||
f.Var(©Options.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given")
|
|
||||||
f.StringArrayVar(©Options.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCopy(opts CopyOptions, gopts GlobalOptions, args []string) error {
|
|
||||||
dstGopts, err := fillSecondaryGlobalOpts(opts.secondaryRepoOptions, gopts, "destination")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
srcRepo, err := OpenRepository(gopts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dstRepo, err := OpenRepository(dstGopts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
srcLock, err := lockRepo(srcRepo)
|
|
||||||
defer unlockRepo(srcLock)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dstLock, err := lockRepo(dstRepo)
|
|
||||||
defer unlockRepo(dstLock)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
debug.Log("Loading source index")
|
|
||||||
if err := srcRepo.LoadIndex(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
debug.Log("Loading destination index")
|
|
||||||
if err := dstRepo.LoadIndex(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot)
|
|
||||||
for sn := range FindFilteredSnapshots(ctx, dstRepo, opts.Hosts, opts.Tags, opts.Paths, nil) {
|
|
||||||
if sn.Original != nil && !sn.Original.IsNull() {
|
|
||||||
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
|
||||||
}
|
|
||||||
// also consider identical snapshot copies
|
|
||||||
dstSnapshotByOriginal[*sn.ID()] = append(dstSnapshotByOriginal[*sn.ID()], sn)
|
|
||||||
}
|
|
||||||
|
|
||||||
cloner := &treeCloner{
|
|
||||||
srcRepo: srcRepo,
|
|
||||||
dstRepo: dstRepo,
|
|
||||||
visitedTrees: restic.NewIDSet(),
|
|
||||||
buf: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
for sn := range FindFilteredSnapshots(ctx, srcRepo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
|
||||||
Verbosef("\nsnapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time)
|
|
||||||
|
|
||||||
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
|
|
||||||
srcOriginal := *sn.ID()
|
|
||||||
if sn.Original != nil {
|
|
||||||
srcOriginal = *sn.Original
|
|
||||||
}
|
|
||||||
if originalSns, ok := dstSnapshotByOriginal[srcOriginal]; ok {
|
|
||||||
isCopy := false
|
|
||||||
for _, originalSn := range originalSns {
|
|
||||||
if similarSnapshots(originalSn, sn) {
|
|
||||||
Verbosef("skipping source snapshot %s, was already copied to snapshot %s\n", sn.ID().Str(), originalSn.ID().Str())
|
|
||||||
isCopy = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isCopy {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Verbosef(" copy started, this may take a while...\n")
|
|
||||||
|
|
||||||
if err := cloner.copyTree(ctx, *sn.Tree); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
debug.Log("tree copied")
|
|
||||||
|
|
||||||
if err = dstRepo.Flush(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
debug.Log("flushed packs and saved index")
|
|
||||||
|
|
||||||
// save snapshot
|
|
||||||
sn.Parent = nil // Parent does not have relevance in the new repo.
|
|
||||||
// Use Original as a persistent snapshot ID
|
|
||||||
if sn.Original == nil {
|
|
||||||
sn.Original = sn.ID()
|
|
||||||
}
|
|
||||||
newID, err := dstRepo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
Verbosef("snapshot %s saved\n", newID.Str())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
|
|
||||||
// everything except Parent and Original must match
|
|
||||||
if !sna.Time.Equal(snb.Time) || !sna.Tree.Equal(*snb.Tree) || sna.Hostname != snb.Hostname ||
|
|
||||||
sna.Username != snb.Username || sna.UID != snb.UID || sna.GID != snb.GID ||
|
|
||||||
len(sna.Paths) != len(snb.Paths) || len(sna.Excludes) != len(snb.Excludes) ||
|
|
||||||
len(sna.Tags) != len(snb.Tags) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !sna.HasPaths(snb.Paths) || !sna.HasTags(snb.Tags) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i, a := range sna.Excludes {
|
|
||||||
if a != snb.Excludes[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type treeCloner struct {
|
|
||||||
srcRepo restic.Repository
|
|
||||||
dstRepo restic.Repository
|
|
||||||
visitedTrees restic.IDSet
|
|
||||||
buf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *treeCloner) copyTree(ctx context.Context, treeID restic.ID) error {
|
|
||||||
// We have already processed this tree
|
|
||||||
if t.visitedTrees.Has(treeID) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tree, err := t.srcRepo.LoadTree(ctx, treeID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("LoadTree(%v) returned error %v", treeID.Str(), err)
|
|
||||||
}
|
|
||||||
t.visitedTrees.Insert(treeID)
|
|
||||||
|
|
||||||
// Do we already have this tree blob?
|
|
||||||
if !t.dstRepo.Index().Has(treeID, restic.TreeBlob) {
|
|
||||||
newTreeID, err := t.dstRepo.SaveTree(ctx, tree)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("SaveTree(%v) returned error %v", treeID.Str(), err)
|
|
||||||
}
|
|
||||||
// Assurance only.
|
|
||||||
if newTreeID != treeID {
|
|
||||||
return fmt.Errorf("SaveTree(%v) returned unexpected id %s", treeID.Str(), newTreeID.Str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: parellize this stuff, likely only needed inside a tree.
|
|
||||||
|
|
||||||
for _, entry := range tree.Nodes {
|
|
||||||
// If it is a directory, recurse
|
|
||||||
if entry.Type == "dir" && entry.Subtree != nil {
|
|
||||||
if err := t.copyTree(ctx, *entry.Subtree); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Copy the blobs for this file.
|
|
||||||
for _, blobID := range entry.Content {
|
|
||||||
// Do we already have this data blob?
|
|
||||||
if t.dstRepo.Index().Has(blobID, restic.DataBlob) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
debug.Log("Copying blob %s\n", blobID.Str())
|
|
||||||
t.buf, err = t.srcRepo.LoadBlob(ctx, restic.DataBlob, blobID, t.buf)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("LoadBlob(%v) returned error %v", blobID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err = t.dstRepo.SaveBlob(ctx, restic.DataBlob, t.buf, blobID, false)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("SaveBlob(%v) returned error %v", blobID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -84,12 +84,12 @@ type Blob struct {
|
|||||||
|
|
||||||
func printPacks(repo *repository.Repository, wr io.Writer) error {
|
func printPacks(repo *repository.Repository, wr io.Writer) error {
|
||||||
|
|
||||||
return repo.List(context.TODO(), restic.PackFile, func(id restic.ID, size int64) error {
|
return repo.List(context.TODO(), restic.DataFile, func(id restic.ID, size int64) error {
|
||||||
h := restic.Handle{Type: restic.PackFile, Name: id.String()}
|
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
|
||||||
|
|
||||||
blobs, err := pack.List(repo.Key(), restic.ReaderAt(repo.Backend(), h), size)
|
blobs, err := pack.List(repo.Key(), restic.ReaderAt(repo.Backend(), h), size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error for pack %v: %v\n", id.Str(), err)
|
fmt.Fprintf(globalOptions.stderr, "error for pack %v: %v\n", id.Str(), err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ func printPacks(repo *repository.Repository, wr io.Writer) error {
|
|||||||
|
|
||||||
func dumpIndexes(repo restic.Repository, wr io.Writer) error {
|
func dumpIndexes(repo restic.Repository, wr io.Writer) error {
|
||||||
return repo.List(context.TODO(), restic.IndexFile, func(id restic.ID, size int64) error {
|
return repo.List(context.TODO(), restic.IndexFile, func(id restic.ID, size int64) error {
|
||||||
Printf("index_id: %v\n", id)
|
fmt.Printf("index_id: %v\n", id)
|
||||||
|
|
||||||
idx, err := repository.LoadIndex(context.TODO(), repo, id)
|
idx, err := repository.LoadIndex(context.TODO(), repo, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -151,13 +151,13 @@ func runDebugDump(gopts GlobalOptions, args []string) error {
|
|||||||
case "packs":
|
case "packs":
|
||||||
return printPacks(repo, gopts.stdout)
|
return printPacks(repo, gopts.stdout)
|
||||||
case "all":
|
case "all":
|
||||||
Printf("snapshots:\n")
|
fmt.Printf("snapshots:\n")
|
||||||
err := debugPrintSnapshots(repo, gopts.stdout)
|
err := debugPrintSnapshots(repo, gopts.stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Printf("\nindexes:\n")
|
fmt.Printf("\nindexes:\n")
|
||||||
err = dumpIndexes(repo, gopts.stdout)
|
err = dumpIndexes(repo, gopts.stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+9
-36
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var cmdDiff = &cobra.Command{
|
var cmdDiff = &cobra.Command{
|
||||||
Use: "diff [flags] snapshot-ID snapshot-ID",
|
Use: "diff snapshot-ID snapshot-ID",
|
||||||
Short: "Show differences between two snapshots",
|
Short: "Show differences between two snapshots",
|
||||||
Long: `
|
Long: `
|
||||||
The "diff" command shows differences from the first to the second snapshot. The
|
The "diff" command shows differences from the first to the second snapshot. The
|
||||||
@@ -116,10 +116,10 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
|||||||
|
|
||||||
// DiffStats collects the differences between two snapshots.
|
// DiffStats collects the differences between two snapshots.
|
||||||
type DiffStats struct {
|
type DiffStats struct {
|
||||||
ChangedFiles int
|
ChangedFiles int
|
||||||
Added DiffStat
|
Added DiffStat
|
||||||
Removed DiffStat
|
Removed DiffStat
|
||||||
BlobsBefore, BlobsAfter, BlobsCommon restic.BlobSet
|
BlobsBefore, BlobsAfter restic.BlobSet
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDiffStats creates new stats for a diff run.
|
// NewDiffStats creates new stats for a diff run.
|
||||||
@@ -127,7 +127,6 @@ func NewDiffStats() *DiffStats {
|
|||||||
return &DiffStats{
|
return &DiffStats{
|
||||||
BlobsBefore: restic.NewBlobSet(),
|
BlobsBefore: restic.NewBlobSet(),
|
||||||
BlobsAfter: restic.NewBlobSet(),
|
BlobsAfter: restic.NewBlobSet(),
|
||||||
BlobsCommon: restic.NewBlobSet(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,27 +177,6 @@ func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, b
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id restic.ID) error {
|
|
||||||
debug.Log("print tree %v", id)
|
|
||||||
tree, err := c.repo.LoadTree(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, node := range tree.Nodes {
|
|
||||||
addBlobs(blobs, node)
|
|
||||||
|
|
||||||
if node.Type == "dir" {
|
|
||||||
err := c.collectDir(ctx, blobs, *node.Subtree)
|
|
||||||
if err != nil {
|
|
||||||
Warnf("error: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[string]*restic.Node, uniqueNames []string) {
|
func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[string]*restic.Node, uniqueNames []string) {
|
||||||
names := make(map[string]struct{})
|
names := make(map[string]struct{})
|
||||||
tree1Nodes = make(map[string]*restic.Node)
|
tree1Nodes = make(map[string]*restic.Node)
|
||||||
@@ -218,7 +196,7 @@ func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[stri
|
|||||||
uniqueNames = append(uniqueNames, name)
|
uniqueNames = append(uniqueNames, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(uniqueNames)
|
sort.Sort(sort.StringSlice(uniqueNames))
|
||||||
return tree1Nodes, tree2Nodes, uniqueNames
|
return tree1Nodes, tree2Nodes, uniqueNames
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,12 +248,7 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStats, prefix string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if node1.Type == "dir" && node2.Type == "dir" {
|
if node1.Type == "dir" && node2.Type == "dir" {
|
||||||
var err error
|
err := c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
||||||
if (*node1.Subtree).Equal(*node2.Subtree) {
|
|
||||||
err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree)
|
|
||||||
} else {
|
|
||||||
err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error: %v\n", err)
|
Warnf("error: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -372,8 +345,8 @@ func runDiff(opts DiffOptions, gopts GlobalOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
|
both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
|
||||||
updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed)
|
updateBlobs(repo, stats.BlobsBefore.Sub(both), &stats.Removed)
|
||||||
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added)
|
updateBlobs(repo, stats.BlobsAfter.Sub(both), &stats.Added)
|
||||||
|
|
||||||
Printf("\n")
|
Printf("\n")
|
||||||
Printf("Files: %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
|
Printf("Files: %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
|
||||||
|
|||||||
+142
-39
@@ -1,16 +1,19 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/dump"
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/walker"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -19,10 +22,8 @@ var cmdDump = &cobra.Command{
|
|||||||
Use: "dump [flags] snapshotID file",
|
Use: "dump [flags] snapshotID file",
|
||||||
Short: "Print a backed-up file to stdout",
|
Short: "Print a backed-up file to stdout",
|
||||||
Long: `
|
Long: `
|
||||||
The "dump" command extracts files from a snapshot from the repository. If a
|
The "dump" command extracts a single file from a snapshot from the repository and
|
||||||
single file is selected, it prints its contents to stdout. Folders are output
|
prints its contents to stdout.
|
||||||
as a tar file containing the contents of the specified folder. Pass "/" as
|
|
||||||
file name to dump the whole snapshot as a tar file.
|
|
||||||
|
|
||||||
The special snapshot "latest" can be used to use the latest snapshot in the
|
The special snapshot "latest" can be used to use the latest snapshot in the
|
||||||
repository.
|
repository.
|
||||||
@@ -58,14 +59,17 @@ func init() {
|
|||||||
|
|
||||||
func splitPath(p string) []string {
|
func splitPath(p string) []string {
|
||||||
d, f := path.Split(p)
|
d, f := path.Split(p)
|
||||||
if d == "" || d == "/" {
|
if d == "" {
|
||||||
return []string{f}
|
return []string{f}
|
||||||
}
|
}
|
||||||
s := splitPath(path.Join("/", d))
|
if d == "/" {
|
||||||
|
return []string{d}
|
||||||
|
}
|
||||||
|
s := splitPath(path.Clean(d))
|
||||||
return append(s, f)
|
return append(s, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string) error {
|
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string, pathToPrint string) error {
|
||||||
|
|
||||||
if tree == nil {
|
if tree == nil {
|
||||||
return fmt.Errorf("called with a nil tree")
|
return fmt.Errorf("called with a nil tree")
|
||||||
@@ -77,42 +81,24 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repositor
|
|||||||
if l == 0 {
|
if l == 0 {
|
||||||
return fmt.Errorf("empty path components")
|
return fmt.Errorf("empty path components")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we print / we need to assume that there are multiple nodes at that
|
|
||||||
// level in the tree.
|
|
||||||
if pathComponents[0] == "" {
|
|
||||||
if err := checkStdoutTar(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return dump.WriteTar(ctx, repo, tree, "/", os.Stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
item := filepath.Join(prefix, pathComponents[0])
|
item := filepath.Join(prefix, pathComponents[0])
|
||||||
for _, node := range tree.Nodes {
|
for _, node := range tree.Nodes {
|
||||||
// If dumping something in the highest level it will just take the
|
if node.Name == pathComponents[0] || pathComponents[0] == "/" {
|
||||||
// first item it finds and dump that according to the switch case below.
|
|
||||||
if node.Name == pathComponents[0] {
|
|
||||||
switch {
|
switch {
|
||||||
case l == 1 && dump.IsFile(node):
|
case l == 1 && node.Type == "file":
|
||||||
return dump.GetNodeData(ctx, os.Stdout, repo, node)
|
return getNodeData(ctx, os.Stdout, repo, node)
|
||||||
case l > 1 && dump.IsDir(node):
|
case l > 1 && node.Type == "dir":
|
||||||
subtree, err := repo.LoadTree(ctx, *node.Subtree)
|
subtree, err := repo.LoadTree(ctx, *node.Subtree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
||||||
}
|
}
|
||||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:])
|
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], pathToPrint)
|
||||||
case dump.IsDir(node):
|
case node.Type == "dir":
|
||||||
if err := checkStdoutTar(); err != nil {
|
node.Path = pathToPrint
|
||||||
return err
|
return tarTree(ctx, repo, node, pathToPrint)
|
||||||
}
|
|
||||||
subtree, err := repo.LoadTree(ctx, *node.Subtree)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return dump.WriteTar(ctx, repo, subtree, item, os.Stdout)
|
|
||||||
case l > 1:
|
case l > 1:
|
||||||
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
||||||
case !dump.IsFile(node):
|
case node.Type != "file":
|
||||||
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +162,7 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
|||||||
Exitf(2, "loading tree for snapshot %q failed: %v", snapshotIDString, err)
|
Exitf(2, "loading tree for snapshot %q failed: %v", snapshotIDString, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = printFromTree(ctx, tree, repo, "/", splittedPath)
|
err = printFromTree(ctx, tree, repo, "", splittedPath, pathToPrint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Exitf(2, "cannot dump file: %v", err)
|
Exitf(2, "cannot dump file: %v", err)
|
||||||
}
|
}
|
||||||
@@ -184,9 +170,126 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkStdoutTar() error {
|
func getNodeData(ctx context.Context, output io.Writer, repo restic.Repository, node *restic.Node) error {
|
||||||
if stdoutIsTerminal() {
|
var (
|
||||||
return fmt.Errorf("stdout is the terminal, please redirect output")
|
buf []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for _, id := range node.Content {
|
||||||
|
buf, err = repo.LoadBlob(ctx, restic.DataBlob, id, buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = output.Write(buf)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Write")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tarTree(ctx context.Context, repo restic.Repository, rootNode *restic.Node, rootPath string) error {
|
||||||
|
|
||||||
|
if stdoutIsTerminal() {
|
||||||
|
return fmt.Errorf("stdout is the terminal, please redirect output")
|
||||||
|
}
|
||||||
|
|
||||||
|
tw := tar.NewWriter(os.Stdout)
|
||||||
|
defer tw.Close()
|
||||||
|
|
||||||
|
// If we want to dump "/" we'll need to add the name of the first node, too
|
||||||
|
// as it would get lost otherwise.
|
||||||
|
if rootNode.Path == "/" {
|
||||||
|
rootNode.Path = path.Join(rootNode.Path, rootNode.Name)
|
||||||
|
rootPath = rootNode.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// we know that rootNode is a folder and walker.Walk will already process
|
||||||
|
// the next node, so we have to tar this one first, too
|
||||||
|
if err := tarNode(ctx, tw, rootNode, repo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := walker.Walk(ctx, repo, *rootNode.Subtree, nil, func(_ restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if node == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Path = path.Join(rootPath, nodepath)
|
||||||
|
|
||||||
|
if node.Type == "file" || node.Type == "symlink" || node.Type == "dir" {
|
||||||
|
err := tarNode(ctx, tw, node, repo)
|
||||||
|
if err != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func tarNode(ctx context.Context, tw *tar.Writer, node *restic.Node, repo restic.Repository) error {
|
||||||
|
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: node.Path,
|
||||||
|
Size: int64(node.Size),
|
||||||
|
Mode: int64(node.Mode),
|
||||||
|
Uid: int(node.UID),
|
||||||
|
Gid: int(node.GID),
|
||||||
|
ModTime: node.ModTime,
|
||||||
|
AccessTime: node.AccessTime,
|
||||||
|
ChangeTime: node.ChangeTime,
|
||||||
|
PAXRecords: parseXattrs(node.ExtendedAttributes),
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Type == "symlink" {
|
||||||
|
header.Typeflag = tar.TypeSymlink
|
||||||
|
header.Linkname = node.LinkTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Type == "dir" {
|
||||||
|
header.Typeflag = tar.TypeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tw.WriteHeader(header)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "TarHeader ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getNodeData(ctx, tw, repo, node)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseXattrs(xattrs []restic.ExtendedAttribute) map[string]string {
|
||||||
|
tmpMap := make(map[string]string)
|
||||||
|
|
||||||
|
for _, attr := range xattrs {
|
||||||
|
attrString := string(attr.Value)
|
||||||
|
|
||||||
|
if strings.HasPrefix(attr.Name, "system.posix_acl_") {
|
||||||
|
na := acl{}
|
||||||
|
na.decode(attr.Value)
|
||||||
|
|
||||||
|
if na.String() != "" {
|
||||||
|
if strings.Contains(attr.Name, "system.posix_acl_access") {
|
||||||
|
tmpMap["SCHILY.acl.access"] = na.String()
|
||||||
|
} else if strings.Contains(attr.Name, "system.posix_acl_default") {
|
||||||
|
tmpMap["SCHILY.acl.default"] = na.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
tmpMap["SCHILY.xattr."+attr.Name] = attrString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpMap
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
rtest "github.com/restic/restic/internal/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDumpSplitPath(t *testing.T) {
|
|
||||||
testPaths := []struct {
|
|
||||||
path string
|
|
||||||
result []string
|
|
||||||
}{
|
|
||||||
{"", []string{""}},
|
|
||||||
{"test", []string{"test"}},
|
|
||||||
{"test/dir", []string{"test", "dir"}},
|
|
||||||
{"test/dir/sub", []string{"test", "dir", "sub"}},
|
|
||||||
{"/", []string{""}},
|
|
||||||
{"/test", []string{"test"}},
|
|
||||||
{"/test/dir", []string{"test", "dir"}},
|
|
||||||
{"/test/dir/sub", []string{"test", "dir", "sub"}},
|
|
||||||
}
|
|
||||||
for _, path := range testPaths {
|
|
||||||
parts := splitPath(path.path)
|
|
||||||
rtest.Equals(t, path.result, parts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -270,7 +270,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
|
|
||||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
||||||
|
|
||||||
return false, walker.ErrSkipNode
|
return false, walker.SkipNode
|
||||||
}
|
}
|
||||||
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
@@ -314,7 +314,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
|
|
||||||
if !childMayMatch {
|
if !childMayMatch {
|
||||||
ignoreIfNoMatch = true
|
ignoreIfNoMatch = true
|
||||||
errIfNoMatch = walker.ErrSkipNode
|
errIfNoMatch = walker.SkipNode
|
||||||
} else {
|
} else {
|
||||||
ignoreIfNoMatch = false
|
ignoreIfNoMatch = false
|
||||||
}
|
}
|
||||||
@@ -354,7 +354,7 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
|||||||
|
|
||||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
||||||
|
|
||||||
return false, walker.ErrSkipNode
|
return false, walker.SkipNode
|
||||||
}
|
}
|
||||||
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
@@ -417,7 +417,7 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
|||||||
packsFound := 0
|
packsFound := 0
|
||||||
|
|
||||||
debug.Log("Looking for packs...")
|
debug.Log("Looking for packs...")
|
||||||
err := f.repo.List(ctx, restic.PackFile, func(id restic.ID, size int64) error {
|
err := f.repo.List(ctx, restic.DataFile, func(id restic.ID, size int64) error {
|
||||||
if allPacksFound {
|
if allPacksFound {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -465,8 +465,8 @@ func (f *Finder) findObjectPack(ctx context.Context, id string, t restic.BlobTyp
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
blobs := idx.Lookup(rid, t)
|
blobs, found := idx.Lookup(rid, t)
|
||||||
if len(blobs) == 0 {
|
if !found {
|
||||||
Printf("Object %s not found in the index\n", rid.Str())
|
Printf("Object %s not found in the index\n", rid.Str())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-26
@@ -94,22 +94,34 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeSnapshots := 0
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var snapshots restic.Snapshots
|
var snapshots restic.Snapshots
|
||||||
removeSnIDs := restic.NewIDSet()
|
|
||||||
|
|
||||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||||
snapshots = append(snapshots, sn)
|
snapshots = append(snapshots, sn)
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonGroups []*ForgetGroup
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
// When explicit snapshots args are given, remove them immediately.
|
// When explicit snapshots args are given, remove them immediately.
|
||||||
for _, sn := range snapshots {
|
for _, sn := range snapshots {
|
||||||
removeSnIDs.Insert(*sn.ID())
|
if !opts.DryRun {
|
||||||
|
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
||||||
|
if err = repo.Backend().Remove(gopts.ctx, h); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !gopts.JSON {
|
||||||
|
Verbosef("removed snapshot %v\n", sn.ID().Str())
|
||||||
|
}
|
||||||
|
removeSnapshots++
|
||||||
|
} else {
|
||||||
|
if !gopts.JSON {
|
||||||
|
Verbosef("would have removed snapshot %v\n", sn.ID().Str())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
||||||
@@ -139,6 +151,8 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|||||||
Verbosef("Applying Policy: %v\n", policy)
|
Verbosef("Applying Policy: %v\n", policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jsonGroups []*ForgetGroup
|
||||||
|
|
||||||
for k, snapshotGroup := range snapshotGroups {
|
for k, snapshotGroup := range snapshotGroups {
|
||||||
if gopts.Verbose >= 1 && !gopts.JSON {
|
if gopts.Verbose >= 1 && !gopts.JSON {
|
||||||
err = PrintSnapshotGroupHeader(gopts.stdout, k)
|
err = PrintSnapshotGroupHeader(gopts.stdout, k)
|
||||||
@@ -177,37 +191,37 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
jsonGroups = append(jsonGroups, &fg)
|
jsonGroups = append(jsonGroups, &fg)
|
||||||
|
|
||||||
for _, sn := range remove {
|
removeSnapshots += len(remove)
|
||||||
removeSnIDs.Insert(*sn.ID())
|
|
||||||
|
if !opts.DryRun {
|
||||||
|
for _, sn := range remove {
|
||||||
|
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
||||||
|
err = repo.Backend().Remove(gopts.ctx, h)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gopts.JSON {
|
||||||
|
err = printJSONForget(gopts.stdout, jsonGroups)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(removeSnIDs) > 0 {
|
if removeSnapshots > 0 && opts.Prune {
|
||||||
|
if !gopts.JSON {
|
||||||
|
Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
|
||||||
|
}
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
err := DeleteFilesChecked(gopts, repo, removeSnIDs, restic.SnapshotFile)
|
return pruneRepository(gopts, repo)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !gopts.JSON {
|
|
||||||
Printf("Would have removed the following snapshots:\n%v\n\n", removeSnIDs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gopts.JSON && len(jsonGroups) > 0 {
|
|
||||||
err = printJSONForget(gopts.stdout, jsonGroups)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(removeSnIDs) > 0 && opts.Prune && !opts.DryRun {
|
|
||||||
return pruneRepository(gopts, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var cmdGenerate = &cobra.Command{
|
var cmdGenerate = &cobra.Command{
|
||||||
Use: "generate [flags]",
|
Use: "generate [command]",
|
||||||
Short: "Generate manual pages and auto-completion files (bash, zsh)",
|
Short: "Generate manual pages and auto-completion files (bash, zsh)",
|
||||||
Long: `
|
Long: `
|
||||||
The "generate" command writes automatically generated files (like the man pages
|
The "generate" command writes automatically generated files (like the man pages
|
||||||
|
|||||||
+3
-43
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/restic/chunker"
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
|
|
||||||
@@ -21,36 +20,19 @@ Exit status is 0 if the command was successful, and non-zero if there was any er
|
|||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runInit(initOptions, globalOptions, args)
|
return runInit(globalOptions, args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitOptions bundles all options for the init command.
|
|
||||||
type InitOptions struct {
|
|
||||||
secondaryRepoOptions
|
|
||||||
CopyChunkerParameters bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var initOptions InitOptions
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmdRoot.AddCommand(cmdInit)
|
cmdRoot.AddCommand(cmdInit)
|
||||||
|
|
||||||
f := cmdInit.Flags()
|
|
||||||
initSecondaryRepoOptions(f, &initOptions.secondaryRepoOptions, "secondary", "to copy chunker parameters from")
|
|
||||||
f.BoolVar(&initOptions.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInit(opts InitOptions, gopts GlobalOptions, args []string) error {
|
func runInit(gopts GlobalOptions, args []string) error {
|
||||||
if gopts.Repo == "" {
|
if gopts.Repo == "" {
|
||||||
return errors.Fatal("Please specify repository location (-r)")
|
return errors.Fatal("Please specify repository location (-r)")
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkerPolynomial, err := maybeReadChunkerPolynomial(opts, gopts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
be, err := create(gopts.Repo, gopts.extended)
|
be, err := create(gopts.Repo, gopts.extended)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("create repository at %s failed: %v\n", gopts.Repo, err)
|
return errors.Fatalf("create repository at %s failed: %v\n", gopts.Repo, err)
|
||||||
@@ -65,7 +47,7 @@ func runInit(opts InitOptions, gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
s := repository.New(be)
|
s := repository.New(be)
|
||||||
|
|
||||||
err = s.Init(gopts.ctx, gopts.password, chunkerPolynomial)
|
err = s.Init(gopts.ctx, gopts.password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("create key in repository at %s failed: %v\n", gopts.Repo, err)
|
return errors.Fatalf("create key in repository at %s failed: %v\n", gopts.Repo, err)
|
||||||
}
|
}
|
||||||
@@ -78,25 +60,3 @@ func runInit(opts InitOptions, gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func maybeReadChunkerPolynomial(opts InitOptions, gopts GlobalOptions) (*chunker.Pol, error) {
|
|
||||||
if opts.CopyChunkerParameters {
|
|
||||||
otherGopts, err := fillSecondaryGlobalOpts(opts.secondaryRepoOptions, gopts, "secondary")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
otherRepo, err := OpenRepository(otherGopts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pol := otherRepo.Config().ChunkerPolynomial
|
|
||||||
return &pol, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Repo != "" {
|
|
||||||
return nil, errors.Fatal("Secondary repository must only be specified when copying the chunker parameters")
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
+6
-12
@@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var cmdKey = &cobra.Command{
|
var cmdKey = &cobra.Command{
|
||||||
Use: "key [flags] [list|add|remove|passwd] [ID]",
|
Use: "key [list|add|remove|passwd] [ID]",
|
||||||
Short: "Manage keys (passwords)",
|
Short: "Manage keys (passwords)",
|
||||||
Long: `
|
Long: `
|
||||||
The "key" command manages keys (passwords) for accessing the repository.
|
The "key" command manages keys (passwords) for accessing the repository.
|
||||||
@@ -32,19 +32,13 @@ Exit status is 0 if the command was successful, and non-zero if there was any er
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var newPasswordFile string
|
||||||
newPasswordFile string
|
|
||||||
keyUsername string
|
|
||||||
keyHostname string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmdRoot.AddCommand(cmdKey)
|
cmdRoot.AddCommand(cmdKey)
|
||||||
|
|
||||||
flags := cmdKey.Flags()
|
flags := cmdKey.Flags()
|
||||||
flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "`file` from which to read the new password")
|
flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "the file from which to load a new password")
|
||||||
flags.StringVarP(&keyUsername, "user", "", "", "the username for new keys")
|
|
||||||
flags.StringVarP(&keyHostname, "host", "", "", "the hostname for new keys")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error {
|
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error {
|
||||||
@@ -116,7 +110,7 @@ func getNewPassword(gopts GlobalOptions) (string, error) {
|
|||||||
newopts.password = ""
|
newopts.password = ""
|
||||||
|
|
||||||
return ReadPasswordTwice(newopts,
|
return ReadPasswordTwice(newopts,
|
||||||
"enter new password: ",
|
"enter password for new key: ",
|
||||||
"enter password again: ")
|
"enter password again: ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +120,7 @@ func addKey(gopts GlobalOptions, repo *repository.Repository) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := repository.AddKey(gopts.ctx, repo, pw, keyUsername, keyHostname, repo.Key())
|
id, err := repository.AddKey(gopts.ctx, repo, pw, repo.Key())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("creating new key failed: %v\n", err)
|
return errors.Fatalf("creating new key failed: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -157,7 +151,7 @@ func changePassword(gopts GlobalOptions, repo *repository.Repository) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := repository.AddKey(gopts.ctx, repo, pw, "", "", repo.Key())
|
id, err := repository.AddKey(gopts.ctx, repo, pw, repo.Key())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("creating new key failed: %v\n", err)
|
return errors.Fatalf("creating new key failed: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-13
@@ -1,15 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/index"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdList = &cobra.Command{
|
var cmdList = &cobra.Command{
|
||||||
Use: "list [flags] [blobs|packs|index|snapshots|keys|locks]",
|
Use: "list [blobs|packs|index|snapshots|keys|locks]",
|
||||||
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.
|
||||||
@@ -50,7 +52,7 @@ func runList(cmd *cobra.Command, opts GlobalOptions, args []string) error {
|
|||||||
var t restic.FileType
|
var t restic.FileType
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "packs":
|
case "packs":
|
||||||
t = restic.PackFile
|
t = restic.DataFile
|
||||||
case "index":
|
case "index":
|
||||||
t = restic.IndexFile
|
t = restic.IndexFile
|
||||||
case "snapshots":
|
case "snapshots":
|
||||||
@@ -60,17 +62,18 @@ func runList(cmd *cobra.Command, opts GlobalOptions, args []string) error {
|
|||||||
case "locks":
|
case "locks":
|
||||||
t = restic.LockFile
|
t = restic.LockFile
|
||||||
case "blobs":
|
case "blobs":
|
||||||
return repo.List(opts.ctx, restic.IndexFile, func(id restic.ID, size int64) error {
|
idx, err := index.Load(opts.ctx, repo, nil)
|
||||||
idx, err := repository.LoadIndex(opts.ctx, repo, id)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
for blobs := range idx.Each(opts.ctx) {
|
|
||||||
Printf("%v %v\n", blobs.Type, blobs.ID)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
|
for _, pack := range idx.Packs {
|
||||||
|
for _, entry := range pack.Entries {
|
||||||
|
fmt.Printf("%v %v\n", entry.Type, entry.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return errors.Fatal("invalid type")
|
return errors.Fatal("invalid type")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var cmdLs = &cobra.Command{
|
var cmdLs = &cobra.Command{
|
||||||
Use: "ls [flags] snapshotID [dir...]",
|
Use: "ls [flags] [snapshotID] [dir...]",
|
||||||
Short: "List files in a snapshot",
|
Short: "List files in a snapshot",
|
||||||
Long: `
|
Long: `
|
||||||
The "ls" command lists files and directories in a snapshot.
|
The "ls" command lists files and directories in a snapshot.
|
||||||
@@ -89,8 +89,8 @@ type lsNode struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 && len(opts.Hosts) == 0 && len(opts.Tags) == 0 && len(opts.Paths) == 0 {
|
||||||
return errors.Fatal("no snapshot ID specified")
|
return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract any specific directories to walk
|
// extract any specific directories to walk
|
||||||
@@ -222,7 +222,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
|||||||
// otherwise, signal the walker to not walk recursively into any
|
// otherwise, signal the walker to not walk recursively into any
|
||||||
// subdirs
|
// subdirs
|
||||||
if node.Type == "dir" {
|
if node.Type == "dir" {
|
||||||
return false, walker.ErrSkipNode
|
return false, walker.SkipNode
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var cmdMigrate = &cobra.Command{
|
var cmdMigrate = &cobra.Command{
|
||||||
Use: "migrate [flags] [name]",
|
Use: "migrate [name]",
|
||||||
Short: "Apply migrations",
|
Short: "Apply migrations",
|
||||||
Long: `
|
Long: `
|
||||||
The "migrate" command applies migrations to a repository. When no migration
|
The "migrate" command applies migrations to a repository. When no migration
|
||||||
|
|||||||
+13
-9
@@ -1,4 +1,7 @@
|
|||||||
// +build darwin freebsd linux
|
// +build !netbsd
|
||||||
|
// +build !openbsd
|
||||||
|
// +build !solaris
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -90,12 +93,10 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gopts.NoLock {
|
lock, err := lockRepo(repo)
|
||||||
lock, err := lockRepo(repo)
|
defer unlockRepo(lock)
|
||||||
defer unlockRepo(lock)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = repo.LoadIndex(gopts.ctx)
|
err = repo.LoadIndex(gopts.ctx)
|
||||||
@@ -141,7 +142,10 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
|||||||
Paths: opts.Paths,
|
Paths: opts.Paths,
|
||||||
SnapshotTemplate: opts.SnapshotTemplate,
|
SnapshotTemplate: opts.SnapshotTemplate,
|
||||||
}
|
}
|
||||||
root := fuse.NewRoot(repo, cfg)
|
root, err := fuse.NewRoot(gopts.ctx, repo, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
Printf("Now serving the repository at %s\n", mountpoint)
|
Printf("Now serving the repository at %s\n", mountpoint)
|
||||||
Printf("When finished, quit with Ctrl-c or umount the mountpoint.\n")
|
Printf("When finished, quit with Ctrl-c or umount the mountpoint.\n")
|
||||||
@@ -179,7 +183,7 @@ func runMount(opts MountOptions, gopts GlobalOptions, args []string) error {
|
|||||||
debug.Log("running umount cleanup handler for mount at %v", mountpoint)
|
debug.Log("running umount cleanup handler for mount at %v", mountpoint)
|
||||||
err := umount(mountpoint)
|
err := umount(mountpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("unable to umount (maybe already umounted or still in use?): %v\n", err)
|
Warnf("unable to umount (maybe already umounted?): %v\n", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
+71
-48
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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/index"
|
"github.com/restic/restic/internal/index"
|
||||||
@@ -44,6 +47,34 @@ func shortenStatus(maxLength int, s string) string {
|
|||||||
return s[:maxLength-3] + "..."
|
return s[:maxLength-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newProgressMax returns a progress that counts blobs.
|
||||||
|
func newProgressMax(show bool, max uint64, description string) *restic.Progress {
|
||||||
|
if !show {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p := restic.NewProgress()
|
||||||
|
|
||||||
|
p.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||||
|
status := fmt.Sprintf("[%s] %s %d / %d %s",
|
||||||
|
formatDuration(d),
|
||||||
|
formatPercent(s.Blobs, max),
|
||||||
|
s.Blobs, max, description)
|
||||||
|
|
||||||
|
if w := stdoutTerminalWidth(); w > 0 {
|
||||||
|
status = shortenStatus(w, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintProgress("%s", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||||
|
fmt.Printf("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
func runPrune(gopts GlobalOptions) error {
|
func runPrune(gopts GlobalOptions) error {
|
||||||
repo, err := OpenRepository(gopts)
|
repo, err := OpenRepository(gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -56,9 +87,6 @@ func runPrune(gopts GlobalOptions) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// we do not need index updates while pruning!
|
|
||||||
repo.DisableAutoIndexUpdate()
|
|
||||||
|
|
||||||
return pruneRepository(gopts, repo)
|
return pruneRepository(gopts, repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +125,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("counting files in repo\n")
|
Verbosef("counting files in repo\n")
|
||||||
err = repo.List(ctx, restic.PackFile, func(restic.ID, int64) error {
|
err = repo.List(ctx, restic.DataFile, func(restic.ID, int64) error {
|
||||||
stats.packs++
|
stats.packs++
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -155,22 +183,34 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
|||||||
|
|
||||||
stats.snapshots = len(snapshots)
|
stats.snapshots = len(snapshots)
|
||||||
|
|
||||||
usedBlobs, err := getUsedBlobs(gopts, repo, snapshots)
|
Verbosef("find data that is still in use for %d snapshots\n", stats.snapshots)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var missingBlobs []restic.BlobHandle
|
usedBlobs := restic.NewBlobSet()
|
||||||
for h := range usedBlobs {
|
seenBlobs := restic.NewBlobSet()
|
||||||
if _, ok := blobCount[h]; !ok {
|
|
||||||
missingBlobs = append(missingBlobs, h)
|
bar = newProgressMax(!gopts.Quiet, uint64(len(snapshots)), "snapshots")
|
||||||
|
bar.Start()
|
||||||
|
for _, sn := range snapshots {
|
||||||
|
debug.Log("process snapshot %v", sn.ID())
|
||||||
|
|
||||||
|
err = restic.FindUsedBlobs(ctx, repo, *sn.Tree, usedBlobs, seenBlobs)
|
||||||
|
if err != nil {
|
||||||
|
if repo.Backend().IsNotExist(err) {
|
||||||
|
return errors.Fatal("unable to load a tree from the repo: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug.Log("processed snapshot %v", sn.ID())
|
||||||
|
bar.Report(restic.Stat{Blobs: 1})
|
||||||
}
|
}
|
||||||
if len(missingBlobs) > 0 {
|
bar.Done()
|
||||||
return errors.Fatalf("%v not found in the new index\n"+
|
|
||||||
"Data blobs seem to be missing, aborting prune to prevent further data loss!\n"+
|
if len(usedBlobs) > stats.blobs {
|
||||||
"Please report this error (along with the output of the 'prune' run) at\n"+
|
return errors.Fatalf("number of used blobs is larger than number of available blobs!\n" +
|
||||||
"https://github.com/restic/restic/issues/new/choose", missingBlobs)
|
"Please report this error (along with the output of the 'prune' run) at\n" +
|
||||||
|
"https://github.com/restic/restic/issues/new")
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("found %d of %d data blobs still in use, removing %d blobs\n",
|
Verbosef("found %d of %d data blobs still in use, removing %d blobs\n",
|
||||||
@@ -238,11 +278,13 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
|||||||
|
|
||||||
var obsoletePacks restic.IDSet
|
var obsoletePacks restic.IDSet
|
||||||
if len(rewritePacks) != 0 {
|
if len(rewritePacks) != 0 {
|
||||||
bar := newProgressMax(!gopts.Quiet, uint64(len(rewritePacks)), "packs rewritten")
|
bar = newProgressMax(!gopts.Quiet, uint64(len(rewritePacks)), "packs rewritten")
|
||||||
|
bar.Start()
|
||||||
obsoletePacks, err = repository.Repack(ctx, repo, rewritePacks, usedBlobs, bar)
|
obsoletePacks, err = repository.Repack(ctx, repo, rewritePacks, usedBlobs, bar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
bar.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
removePacks.Merge(obsoletePacks)
|
removePacks.Merge(obsoletePacks)
|
||||||
@@ -252,38 +294,19 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(removePacks) != 0 {
|
if len(removePacks) != 0 {
|
||||||
Verbosef("remove %d old packs\n", len(removePacks))
|
bar = newProgressMax(!gopts.Quiet, uint64(len(removePacks)), "packs deleted")
|
||||||
DeleteFiles(gopts, repo, removePacks, restic.PackFile)
|
bar.Start()
|
||||||
|
for packID := range removePacks {
|
||||||
|
h := restic.Handle{Type: restic.DataFile, Name: packID.String()}
|
||||||
|
err = repo.Backend().Remove(ctx, h)
|
||||||
|
if err != nil {
|
||||||
|
Warnf("unable to remove file %v from the repository\n", packID.Str())
|
||||||
|
}
|
||||||
|
bar.Report(restic.Stat{Blobs: 1})
|
||||||
|
}
|
||||||
|
bar.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("done\n")
|
Verbosef("done\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUsedBlobs(gopts GlobalOptions, repo restic.Repository, snapshots []*restic.Snapshot) (usedBlobs restic.BlobSet, err error) {
|
|
||||||
ctx := gopts.ctx
|
|
||||||
|
|
||||||
Verbosef("find data that is still in use for %d snapshots\n", len(snapshots))
|
|
||||||
|
|
||||||
usedBlobs = restic.NewBlobSet()
|
|
||||||
|
|
||||||
bar := newProgressMax(!gopts.Quiet, uint64(len(snapshots)), "snapshots")
|
|
||||||
bar.Start()
|
|
||||||
defer bar.Done()
|
|
||||||
for _, sn := range snapshots {
|
|
||||||
debug.Log("process snapshot %v", sn.ID())
|
|
||||||
|
|
||||||
err = restic.FindUsedBlobs(ctx, repo, *sn.Tree, usedBlobs)
|
|
||||||
if err != nil {
|
|
||||||
if repo.Backend().IsNotExist(err) {
|
|
||||||
return nil, errors.Fatal("unable to load a tree from the repo: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
debug.Log("processed snapshot %v", sn.ID())
|
|
||||||
bar.Report(restic.Stat{Blobs: 1})
|
|
||||||
}
|
|
||||||
return usedBlobs, nil
|
|
||||||
}
|
|
||||||
@@ -53,7 +53,7 @@ func rebuildIndex(ctx context.Context, repo restic.Repository, ignorePacks resti
|
|||||||
Verbosef("counting files in repo\n")
|
Verbosef("counting files in repo\n")
|
||||||
|
|
||||||
var packs uint64
|
var packs uint64
|
||||||
err := repo.List(ctx, restic.PackFile, func(restic.ID, int64) error {
|
err := repo.List(ctx, restic.DataFile, func(restic.ID, int64) error {
|
||||||
packs++
|
packs++
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -92,9 +92,14 @@ func rebuildIndex(ctx context.Context, repo restic.Repository, ignorePacks resti
|
|||||||
Verbosef("saved new indexes as %v\n", ids)
|
Verbosef("saved new indexes as %v\n", ids)
|
||||||
|
|
||||||
Verbosef("remove %d old index files\n", len(supersedes))
|
Verbosef("remove %d old index files\n", len(supersedes))
|
||||||
err = DeleteFilesChecked(globalOptions, repo, restic.NewIDSet(supersedes...), restic.IndexFile)
|
|
||||||
if err != nil {
|
for _, id := range supersedes {
|
||||||
return errors.Fatalf("unable to remove an old index: %v\n", err)
|
if err := repo.Backend().Remove(ctx, restic.Handle{
|
||||||
|
Type: restic.IndexFile,
|
||||||
|
Name: id.String(),
|
||||||
|
}); err != nil {
|
||||||
|
Warnf("error removing old index %v: %v\n", id.Str(), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ func runRecover(gopts GlobalOptions) error {
|
|||||||
return errors.Fatalf("unable to save blobs to the repo: %v", err)
|
return errors.Fatalf("unable to save blobs to the repo: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = repo.SaveIndex(gopts.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Fatalf("unable to save new index to the repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
sn, err := restic.NewSnapshot([]string{"/recover"}, []string{}, hostname, time.Now())
|
sn, err := restic.NewSnapshot([]string{"/recover"}, []string{}, 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)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var cmdSnapshots = &cobra.Command{
|
var cmdSnapshots = &cobra.Command{
|
||||||
Use: "snapshots [flags] [snapshotID ...]",
|
Use: "snapshots [snapshotID ...]",
|
||||||
Short: "List all snapshots",
|
Short: "List all snapshots",
|
||||||
Long: `
|
Long: `
|
||||||
The "snapshots" command lists all snapshots stored in the repository.
|
The "snapshots" command lists all snapshots stored in the repository.
|
||||||
@@ -251,8 +251,9 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
|
|||||||
// Prints nothing, if we did not group at all.
|
// Prints nothing, if we did not group at all.
|
||||||
func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
|
func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
|
||||||
var key restic.SnapshotGroupKey
|
var key restic.SnapshotGroupKey
|
||||||
|
var err error
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(groupKeyJSON), &key)
|
err = json.Unmarshal([]byte(groupKeyJSON), &key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-60
@@ -2,31 +2,31 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdStats = &cobra.Command{
|
var cmdStats = &cobra.Command{
|
||||||
Use: "stats [flags] [snapshot ID] [...]",
|
Use: "stats [flags] [snapshot-ID]",
|
||||||
Short: "Scan the repository and show basic statistics",
|
Short: "Scan the repository and show basic statistics",
|
||||||
Long: `
|
Long: `
|
||||||
The "stats" command walks one or multiple snapshots in a repository
|
The "stats" command walks one or all snapshots in a repository and
|
||||||
and accumulates statistics about the data stored therein. It reports
|
accumulates statistics about the data stored therein. It reports on
|
||||||
on the number of unique files and their sizes, according to one of
|
the number of unique files and their sizes, according to one of
|
||||||
the counting modes as given by the --mode flag.
|
the counting modes as given by the --mode flag.
|
||||||
|
|
||||||
It operates on all snapshots matching the selection criteria or all
|
If no snapshot is specified, all snapshots will be considered. Some
|
||||||
snapshots if nothing is specified. The special snapshot ID "latest"
|
modes make more sense over just a single snapshot, while others
|
||||||
is also supported. Some modes make more sense over
|
are useful across all snapshots, depending on what you are trying
|
||||||
just a single snapshot, while others are useful across all snapshots,
|
to calculate.
|
||||||
depending on what you are trying to calculate.
|
|
||||||
|
|
||||||
The modes are:
|
The modes are:
|
||||||
|
|
||||||
@@ -50,26 +50,11 @@ Exit status is 0 if the command was successful, and non-zero if there was any er
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatsOptions collects all options for the stats command.
|
|
||||||
type StatsOptions struct {
|
|
||||||
// the mode of counting to perform (see consts for available modes)
|
|
||||||
countMode string
|
|
||||||
|
|
||||||
// filter snapshots by, if given by user
|
|
||||||
Hosts []string
|
|
||||||
Tags restic.TagLists
|
|
||||||
Paths []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var statsOptions StatsOptions
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmdRoot.AddCommand(cmdStats)
|
cmdRoot.AddCommand(cmdStats)
|
||||||
f := cmdStats.Flags()
|
f := cmdStats.Flags()
|
||||||
f.StringVar(&statsOptions.countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file or raw-data")
|
f.StringVar(&countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file, or raw-data")
|
||||||
f.StringArrayVarP(&statsOptions.Hosts, "host", "H", nil, "only consider snapshots with the given `host` (can be specified multiple times)")
|
f.StringArrayVarP(&snapshotByHosts, "host", "H", nil, "filter latest snapshot by this hostname (can be specified multiple times)")
|
||||||
f.Var(&statsOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")
|
|
||||||
f.StringArrayVar(&statsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runStats(gopts GlobalOptions, args []string) error {
|
func runStats(gopts GlobalOptions, args []string) error {
|
||||||
@@ -104,25 +89,53 @@ func runStats(gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
// create a container for the stats (and other needed state)
|
// create a container for the stats (and other needed state)
|
||||||
stats := &statsContainer{
|
stats := &statsContainer{
|
||||||
uniqueFiles: make(map[fileID]struct{}),
|
uniqueFiles: make(map[fileID]struct{}),
|
||||||
uniqueInodes: make(map[uint64]struct{}),
|
uniqueInodes: make(map[uint64]struct{}),
|
||||||
fileBlobs: make(map[string]restic.IDSet),
|
fileBlobs: make(map[string]restic.IDSet),
|
||||||
blobs: restic.NewBlobSet(),
|
blobs: restic.NewBlobSet(),
|
||||||
snapshotsCount: 0,
|
blobsSeen: restic.NewBlobSet(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for sn := range FindFilteredSnapshots(ctx, repo, statsOptions.Hosts, statsOptions.Tags, statsOptions.Paths, args) {
|
if snapshotIDString != "" {
|
||||||
err = statsWalkSnapshot(ctx, sn, repo, stats)
|
// scan just a single snapshot
|
||||||
|
|
||||||
|
var sID restic.ID
|
||||||
|
if snapshotIDString == "latest" {
|
||||||
|
sID, err = restic.FindLatestSnapshot(ctx, repo, []string{}, []restic.TagList{}, snapshotByHosts)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Fatalf("latest snapshot for criteria not found: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sID, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Fatalf("error loading snapshot: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot, err := restic.LoadSnapshot(ctx, repo, sID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Fatalf("error loading snapshot from repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = statsWalkSnapshot(ctx, snapshot, repo, stats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error walking snapshot: %v", err)
|
return fmt.Errorf("error walking snapshot: %v", err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// iterate every snapshot in the repo
|
||||||
|
err = repo.List(ctx, restic.SnapshotFile, func(snapshotID restic.ID, size int64) error {
|
||||||
|
snapshot, err := restic.LoadSnapshot(ctx, repo, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error loading snapshot %s: %v", snapshotID.Str(), err)
|
||||||
|
}
|
||||||
|
return statsWalkSnapshot(ctx, snapshot, repo, stats)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if statsOptions.countMode == countModeRawData {
|
if 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 {
|
for blobHandle := range stats.blobs {
|
||||||
blobSize, found := repo.LookupBlobSize(blobHandle.ID, blobHandle.Type)
|
blobSize, found := repo.LookupBlobSize(blobHandle.ID, blobHandle.Type)
|
||||||
@@ -135,23 +148,29 @@ func runStats(gopts GlobalOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
err = json.NewEncoder(globalOptions.stdout).Encode(stats)
|
err = json.NewEncoder(os.Stdout).Encode(stats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encoding output: %v", err)
|
return fmt.Errorf("encoding output: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
Printf("Stats in %s mode:\n", statsOptions.countMode)
|
// inform the user what was scanned and how it was scanned
|
||||||
Printf("Snapshots processed: %d\n", stats.snapshotsCount)
|
snapshotsScanned := snapshotIDString
|
||||||
|
if snapshotsScanned == "latest" {
|
||||||
|
snapshotsScanned = "the latest snapshot"
|
||||||
|
} else if snapshotsScanned == "" {
|
||||||
|
snapshotsScanned = "all snapshots"
|
||||||
|
}
|
||||||
|
Printf("Stats for %s in %s mode:\n", snapshotsScanned, countMode)
|
||||||
|
|
||||||
if stats.TotalBlobCount > 0 {
|
if stats.TotalBlobCount > 0 {
|
||||||
Printf(" Total Blob Count: %d\n", stats.TotalBlobCount)
|
Printf(" Total Blob Count: %d\n", stats.TotalBlobCount)
|
||||||
}
|
}
|
||||||
if stats.TotalFileCount > 0 {
|
if stats.TotalFileCount > 0 {
|
||||||
Printf(" Total File Count: %d\n", stats.TotalFileCount)
|
Printf(" Total File Count: %d\n", stats.TotalFileCount)
|
||||||
}
|
}
|
||||||
Printf(" Total Size: %-5s\n", formatBytes(stats.TotalSize))
|
Printf(" Total Size: %-5s\n", formatBytes(stats.TotalSize))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -161,19 +180,16 @@ func statsWalkSnapshot(ctx context.Context, snapshot *restic.Snapshot, repo rest
|
|||||||
return fmt.Errorf("snapshot %s has nil tree", snapshot.ID().Str())
|
return fmt.Errorf("snapshot %s has nil tree", snapshot.ID().Str())
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.snapshotsCount++
|
if countMode == countModeRawData {
|
||||||
|
|
||||||
if statsOptions.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 restic.FindUsedBlobs(ctx, repo, *snapshot.Tree, stats.blobs)
|
return restic.FindUsedBlobs(ctx, repo, *snapshot.Tree, stats.blobs, stats.blobsSeen)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := walker.Walk(ctx, repo, *snapshot.Tree, restic.NewIDSet(), statsWalkTree(repo, stats))
|
err := walker.Walk(ctx, repo, *snapshot.Tree, restic.NewIDSet(), statsWalkTree(repo, stats))
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,19 +202,19 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if statsOptions.countMode == countModeUniqueFilesByContents || statsOptions.countMode == countModeBlobsPerFile {
|
if countMode == countModeUniqueFilesByContents || 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)
|
||||||
if _, ok := stats.uniqueFiles[fid]; !ok {
|
if _, ok := stats.uniqueFiles[fid]; !ok {
|
||||||
// mark the file as visited
|
// mark the file as visited
|
||||||
stats.uniqueFiles[fid] = struct{}{}
|
stats.uniqueFiles[fid] = struct{}{}
|
||||||
|
|
||||||
if statsOptions.countMode == countModeUniqueFilesByContents {
|
if countMode == countModeUniqueFilesByContents {
|
||||||
// simply count the size of each unique file (unique by contents only)
|
// simply count the size of each unique file (unique by contents only)
|
||||||
stats.TotalSize += node.Size
|
stats.TotalSize += node.Size
|
||||||
stats.TotalFileCount++
|
stats.TotalFileCount++
|
||||||
}
|
}
|
||||||
if statsOptions.countMode == countModeBlobsPerFile {
|
if countMode == countModeBlobsPerFile {
|
||||||
// count the size of each unique blob reference, which is
|
// count the size of each unique blob reference, which is
|
||||||
// by unique file (unique by contents and file path)
|
// by unique file (unique by contents and file path)
|
||||||
for _, blobID := range node.Content {
|
for _, blobID := range node.Content {
|
||||||
@@ -228,7 +244,7 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if statsOptions.countMode == countModeRestoreSize {
|
if countMode == countModeRestoreSize {
|
||||||
// as this is a file in the snapshot, we can simply count its
|
// as this is a file in the snapshot, we can simply count its
|
||||||
// size without worrying about uniqueness, since duplicate files
|
// size without worrying about uniqueness, since duplicate files
|
||||||
// will still be restored
|
// will still be restored
|
||||||
@@ -260,13 +276,23 @@ func makeFileIDByContents(node *restic.Node) fileID {
|
|||||||
|
|
||||||
func verifyStatsInput(gopts GlobalOptions, args []string) error {
|
func verifyStatsInput(gopts GlobalOptions, args []string) error {
|
||||||
// require a recognized counting mode
|
// require a recognized counting mode
|
||||||
switch statsOptions.countMode {
|
switch countMode {
|
||||||
case countModeRestoreSize:
|
case countModeRestoreSize:
|
||||||
case countModeUniqueFilesByContents:
|
case countModeUniqueFilesByContents:
|
||||||
case countModeBlobsPerFile:
|
case countModeBlobsPerFile:
|
||||||
case countModeRawData:
|
case countModeRawData:
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown counting mode: %s (use the -h flag to get a list of supported modes)", statsOptions.countMode)
|
return fmt.Errorf("unknown counting mode: %s (use the -h flag to get a list of supported modes)", countMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure at most one snapshot was specified
|
||||||
|
if len(args) > 1 {
|
||||||
|
return fmt.Errorf("only one snapshot may be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a snapshot was specified, mark it as the one to scan
|
||||||
|
if len(args) == 1 {
|
||||||
|
snapshotIDString = args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -292,17 +318,26 @@ type statsContainer struct {
|
|||||||
// blobs that have been seen as a part of the file
|
// blobs that have been seen as a part of the file
|
||||||
fileBlobs map[string]restic.IDSet
|
fileBlobs map[string]restic.IDSet
|
||||||
|
|
||||||
// blobs is used to count individual unique blobs,
|
// blobs and blobsSeen are used to count individual
|
||||||
// independent of references to files
|
// unique blobs, independent of references to files
|
||||||
blobs restic.BlobSet
|
blobs, blobsSeen restic.BlobSet
|
||||||
|
|
||||||
// holds count of all considered snapshots
|
|
||||||
snapshotsCount int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
|
var (
|
||||||
|
// the mode of counting to perform
|
||||||
|
countMode string
|
||||||
|
|
||||||
|
// the snapshot to scan, as given by the user
|
||||||
|
snapshotIDString string
|
||||||
|
|
||||||
|
// snapshotByHost is the host to filter latest
|
||||||
|
// snapshot by, if given by user
|
||||||
|
snapshotByHosts []string
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
countModeRestoreSize = "restore-size"
|
countModeRestoreSize = "restore-size"
|
||||||
countModeUniqueFilesByContents = "files-by-contents"
|
countModeUniqueFilesByContents = "files-by-contents"
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
|
|
||||||
"github.com/restic/restic/internal/restic"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeleteFiles deletes the given fileList of fileType in parallel
|
|
||||||
// it will print a warning if there is an error, but continue deleting the remaining files
|
|
||||||
func DeleteFiles(gopts GlobalOptions, repo restic.Repository, fileList restic.IDSet, fileType restic.FileType) {
|
|
||||||
deleteFiles(gopts, true, repo, fileList, fileType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteFilesChecked deletes the given fileList of fileType in parallel
|
|
||||||
// if an error occurs, it will cancel and return this error
|
|
||||||
func DeleteFilesChecked(gopts GlobalOptions, repo restic.Repository, fileList restic.IDSet, fileType restic.FileType) error {
|
|
||||||
return deleteFiles(gopts, false, repo, fileList, fileType)
|
|
||||||
}
|
|
||||||
|
|
||||||
const numDeleteWorkers = 8
|
|
||||||
|
|
||||||
// deleteFiles deletes the given fileList of fileType in parallel
|
|
||||||
// if ignoreError=true, it will print a warning if there was an error, else it will abort.
|
|
||||||
func deleteFiles(gopts GlobalOptions, ignoreError bool, repo restic.Repository, fileList restic.IDSet, fileType restic.FileType) error {
|
|
||||||
totalCount := len(fileList)
|
|
||||||
fileChan := make(chan restic.ID)
|
|
||||||
go func() {
|
|
||||||
for id := range fileList {
|
|
||||||
fileChan <- id
|
|
||||||
}
|
|
||||||
close(fileChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
bar := newProgressMax(!gopts.JSON && !gopts.Quiet, uint64(totalCount), "files deleted")
|
|
||||||
wg, ctx := errgroup.WithContext(gopts.ctx)
|
|
||||||
bar.Start()
|
|
||||||
for i := 0; i < numDeleteWorkers; i++ {
|
|
||||||
wg.Go(func() error {
|
|
||||||
for id := range fileChan {
|
|
||||||
h := restic.Handle{Type: fileType, Name: id.String()}
|
|
||||||
err := repo.Backend().Remove(ctx, h)
|
|
||||||
if err != nil {
|
|
||||||
if !gopts.JSON {
|
|
||||||
Warnf("unable to remove %v from the repository\n", h)
|
|
||||||
}
|
|
||||||
if !ignoreError {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !gopts.JSON && gopts.verbosity >= 2 {
|
|
||||||
Verbosef("removed %v\n", h)
|
|
||||||
}
|
|
||||||
bar.Report(restic.Stat{Blobs: 1})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
err := wg.Wait()
|
|
||||||
bar.Done()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
+2
-50
@@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -132,7 +131,7 @@ func rejectIfPresent(excludeFileSpec string) (RejectByNameFunc, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isExcludedByFile interprets filename as a path and returns true if that file
|
// isExcludedByFile interprets filename as a path and returns true if that file
|
||||||
// is in an excluded directory. A directory is identified as excluded if it contains a
|
// is in a excluded directory. A directory is identified as excluded if it contains a
|
||||||
// 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.
|
||||||
@@ -191,7 +190,7 @@ func isDirExcludedByFile(dir, tagFilename, header string) bool {
|
|||||||
Warnf("could not read signature from exclusion tagfile %q: %v\n", tf, err)
|
Warnf("could not read signature from exclusion tagfile %q: %v\n", tf, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !bytes.Equal(buf, []byte(header)) {
|
if bytes.Compare(buf, []byte(header)) != 0 {
|
||||||
Warnf("invalid signature in exclusion tagfile %q\n", tf)
|
Warnf("invalid signature in exclusion tagfile %q\n", tf)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -293,50 +292,3 @@ func rejectResticCache(repo *repository.Repository) (RejectByNameFunc, error) {
|
|||||||
return false
|
return false
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rejectBySize(maxSizeStr string) (RejectFunc, error) {
|
|
||||||
maxSize, err := parseSizeStr(maxSizeStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(item string, fi os.FileInfo) bool {
|
|
||||||
// directory will be ignored
|
|
||||||
if fi.IsDir() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
filesize := fi.Size()
|
|
||||||
if filesize > maxSize {
|
|
||||||
debug.Log("file %s is oversize: %d", item, filesize)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSizeStr(sizeStr string) (int64, error) {
|
|
||||||
numStr := sizeStr[:len(sizeStr)-1]
|
|
||||||
var unit int64 = 1
|
|
||||||
|
|
||||||
switch sizeStr[len(sizeStr)-1] {
|
|
||||||
case 'b', 'B':
|
|
||||||
// use initialized values, do nothing here
|
|
||||||
case 'k', 'K':
|
|
||||||
unit = 1024
|
|
||||||
case 'm', 'M':
|
|
||||||
unit = 1024 * 1024
|
|
||||||
case 'g', 'G':
|
|
||||||
unit = 1024 * 1024 * 1024
|
|
||||||
case 't', 'T':
|
|
||||||
unit = 1024 * 1024 * 1024 * 1024
|
|
||||||
default:
|
|
||||||
numStr = sizeStr
|
|
||||||
}
|
|
||||||
value, err := strconv.ParseInt(numStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
return value * unit, nil
|
|
||||||
}
|
|
||||||
@@ -189,113 +189,3 @@ func TestMultipleIsExcludedByFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseSizeStr(t *testing.T) {
|
|
||||||
sizeStrTests := []struct {
|
|
||||||
in string
|
|
||||||
expected int64
|
|
||||||
}{
|
|
||||||
{"1024", 1024},
|
|
||||||
{"1024b", 1024},
|
|
||||||
{"1024B", 1024},
|
|
||||||
{"1k", 1024},
|
|
||||||
{"100k", 102400},
|
|
||||||
{"100K", 102400},
|
|
||||||
{"10M", 10485760},
|
|
||||||
{"100m", 104857600},
|
|
||||||
{"20G", 21474836480},
|
|
||||||
{"10g", 10737418240},
|
|
||||||
{"2T", 2199023255552},
|
|
||||||
{"2t", 2199023255552},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range sizeStrTests {
|
|
||||||
actual, err := parseSizeStr(tt.in)
|
|
||||||
test.OK(t, err)
|
|
||||||
|
|
||||||
if actual != tt.expected {
|
|
||||||
t.Errorf("parseSizeStr(%s) = %d; expected %d", tt.in, actual, tt.expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIsExcludedByFileSize is for testing the instance of
|
|
||||||
// --exclude-larger-than parameters
|
|
||||||
func TestIsExcludedByFileSize(t *testing.T) {
|
|
||||||
tempDir, cleanup := test.TempDir(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
// Max size of file is set to be 1k
|
|
||||||
maxSizeStr := "1k"
|
|
||||||
|
|
||||||
// Create some files in a temporary directory.
|
|
||||||
// Files in UPPERCASE will be used as exclusion triggers later on.
|
|
||||||
// We will test the inclusion later, so we add the expected value as
|
|
||||||
// a bool.
|
|
||||||
files := []struct {
|
|
||||||
path string
|
|
||||||
size int64
|
|
||||||
incl bool
|
|
||||||
}{
|
|
||||||
{"42", 100, true},
|
|
||||||
|
|
||||||
// everything in foodir except the FOOLARGE tagfile
|
|
||||||
// should not be included.
|
|
||||||
{"foodir/FOOLARGE", 2048, false},
|
|
||||||
{"foodir/foo", 1002, true},
|
|
||||||
{"foodir/foosub/underfoo", 100, true},
|
|
||||||
|
|
||||||
// everything in bardir except the BARLARGE tagfile
|
|
||||||
// should not be included.
|
|
||||||
{"bardir/BARLARGE", 1030, false},
|
|
||||||
{"bardir/bar", 1000, true},
|
|
||||||
{"bardir/barsub/underbar", 500, true},
|
|
||||||
|
|
||||||
// everything in bazdir should be included.
|
|
||||||
{"bazdir/baz", 100, true},
|
|
||||||
{"bazdir/bazsub/underbaz", 200, true},
|
|
||||||
}
|
|
||||||
var errs []error
|
|
||||||
for _, f := range files {
|
|
||||||
// create directories first, then the file
|
|
||||||
p := filepath.Join(tempDir, filepath.FromSlash(f.path))
|
|
||||||
errs = append(errs, os.MkdirAll(filepath.Dir(p), 0700))
|
|
||||||
file, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
errs = append(errs, err)
|
|
||||||
if err == nil {
|
|
||||||
// create a file with given size
|
|
||||||
errs = append(errs, file.Truncate(f.size))
|
|
||||||
}
|
|
||||||
errs = append(errs, file.Close())
|
|
||||||
}
|
|
||||||
test.OKs(t, errs) // see if anything went wrong during the creation
|
|
||||||
|
|
||||||
// create rejection function
|
|
||||||
sizeExclude, _ := rejectBySize(maxSizeStr)
|
|
||||||
|
|
||||||
// To mock the archiver scanning walk, we create filepath.WalkFn
|
|
||||||
// that tests against the two rejection functions and stores
|
|
||||||
// the result in a map against we can test later.
|
|
||||||
m := make(map[string]bool)
|
|
||||||
walk := func(p string, fi os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
excluded := sizeExclude(p, fi)
|
|
||||||
// the log message helps debugging in case the test fails
|
|
||||||
t.Logf("%q: dir:%t; size:%d; excluded:%v", p, fi.IsDir(), fi.Size(), excluded)
|
|
||||||
m[p] = !excluded
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// walk through the temporary file and check the error
|
|
||||||
test.OK(t, filepath.Walk(tempDir, walk))
|
|
||||||
|
|
||||||
// compare whether the walk gave the expected values for the test cases
|
|
||||||
for _, f := range files {
|
|
||||||
p := filepath.Join(tempDir, filepath.FromSlash(f.path))
|
|
||||||
if m[p] != f.incl {
|
|
||||||
t.Errorf("inclusion status of %s is wrong: want %v, got %v", f.path, f.incl, m[p])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -22,10 +22,10 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
|
|||||||
// Process all snapshot IDs given as arguments.
|
// Process all snapshot IDs given as arguments.
|
||||||
for _, s := range snapshotIDs {
|
for _, s := range snapshotIDs {
|
||||||
if s == "latest" {
|
if s == "latest" {
|
||||||
usedFilter = true
|
|
||||||
id, err = restic.FindLatestSnapshot(ctx, repo, paths, tags, hosts)
|
id, err = restic.FindLatestSnapshot(ctx, repo, paths, tags, hosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Hosts:%v)\n", s, paths, tags, hosts)
|
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Hosts:%v)\n", s, paths, tags, hosts)
|
||||||
|
usedFilter = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ func formatPercent(numerator uint64, denominator uint64) string {
|
|||||||
return fmt.Sprintf("%3.2f%%", percent)
|
return fmt.Sprintf("%3.2f%%", percent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatRate(bytes uint64, duration time.Duration) string {
|
||||||
|
sec := float64(duration) / float64(time.Second)
|
||||||
|
rate := float64(bytes) / sec / (1 << 20)
|
||||||
|
return fmt.Sprintf("%.2fMiB/s", rate)
|
||||||
|
}
|
||||||
|
|
||||||
func formatDuration(d time.Duration) string {
|
func formatDuration(d time.Duration) string {
|
||||||
sec := uint64(d / time.Second)
|
sec := uint64(d / time.Second)
|
||||||
return formatSeconds(sec)
|
return formatSeconds(sec)
|
||||||
|
|||||||
+7
-19
@@ -39,13 +39,11 @@ import (
|
|||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.10.0"
|
var version = "0.9.6-dev (compiled manually)"
|
||||||
|
|
||||||
// TimeFormat is the format used for all timestamps printed by restic.
|
// TimeFormat is the format used for all timestamps printed by restic.
|
||||||
const TimeFormat = "2006-01-02 15:04:05"
|
const TimeFormat = "2006-01-02 15:04:05"
|
||||||
|
|
||||||
type backendWrapper func(r restic.Backend) (restic.Backend, error)
|
|
||||||
|
|
||||||
// GlobalOptions hold all global options for restic.
|
// GlobalOptions hold all global options for restic.
|
||||||
type GlobalOptions struct {
|
type GlobalOptions struct {
|
||||||
Repo string
|
Repo string
|
||||||
@@ -70,13 +68,11 @@ type GlobalOptions struct {
|
|||||||
stdout io.Writer
|
stdout io.Writer
|
||||||
stderr io.Writer
|
stderr io.Writer
|
||||||
|
|
||||||
backendTestHook backendWrapper
|
|
||||||
|
|
||||||
// verbosity is set as follows:
|
// verbosity is set as follows:
|
||||||
// 0 means: don't print any messages except errors, this is used when --quiet is specified
|
// 0 means: don't print any messages except errors, this is used when --quiet is specified
|
||||||
// 1 is the default: print essential messages
|
// 1 is the default: print essential messages
|
||||||
// 2 means: print more messages, report minor things, this is used when --verbose is specified
|
// 2 means: print more messages, report minor things, this is used when --verbose is specified
|
||||||
// 3 means: print very detailed debug messages, this is used when --verbose=2 is specified
|
// 3 means: print very detailed debug messages, this is used when --verbose 2 is specified
|
||||||
verbosity uint
|
verbosity uint
|
||||||
|
|
||||||
Options []string
|
Options []string
|
||||||
@@ -101,11 +97,11 @@ func init() {
|
|||||||
|
|
||||||
f := cmdRoot.PersistentFlags()
|
f := cmdRoot.PersistentFlags()
|
||||||
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "`repository` to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "`repository` to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
||||||
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "`file` to read the repository password from (default: $RESTIC_PASSWORD_FILE)")
|
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a `file` (default: $RESTIC_PASSWORD_FILE)")
|
||||||
f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "`key` ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
|
f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "`key` ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
|
||||||
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "shell `command` to obtain the repository password from (default: $RESTIC_PASSWORD_COMMAND)")
|
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "specify a shell `command` to obtain a password (default: $RESTIC_PASSWORD_COMMAND)")
|
||||||
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
|
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
|
||||||
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level --verbose=`n`)")
|
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
|
||||||
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
|
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
|
||||||
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
|
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
|
||||||
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache `directory`. (default: use system default cache directory)")
|
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache `directory`. (default: use system default cache directory)")
|
||||||
@@ -274,7 +270,7 @@ func Exitf(exitcode int, format string, args ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolvePassword determines the password to be used for opening the repository.
|
// resolvePassword determines the password to be used for opening the repository.
|
||||||
func resolvePassword(opts GlobalOptions, envStr string) (string, error) {
|
func resolvePassword(opts GlobalOptions) (string, error) {
|
||||||
if opts.PasswordFile != "" && opts.PasswordCommand != "" {
|
if opts.PasswordFile != "" && opts.PasswordCommand != "" {
|
||||||
return "", errors.Fatalf("Password file and command are mutually exclusive options")
|
return "", errors.Fatalf("Password file and command are mutually exclusive options")
|
||||||
}
|
}
|
||||||
@@ -299,7 +295,7 @@ func resolvePassword(opts GlobalOptions, envStr string) (string, error) {
|
|||||||
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
||||||
}
|
}
|
||||||
|
|
||||||
if pwd := os.Getenv(envStr); pwd != "" {
|
if pwd := os.Getenv("RESTIC_PASSWORD"); pwd != "" {
|
||||||
return pwd, nil
|
return pwd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,14 +395,6 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
|||||||
Warnf("%v returned error, retrying after %v: %v\n", msg, d, err)
|
Warnf("%v returned error, retrying after %v: %v\n", msg, d, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
// wrap backend if a test specified a hook
|
|
||||||
if opts.backendTestHook != nil {
|
|
||||||
be, err = opts.backendTestHook(be)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s := repository.New(be)
|
s := repository.New(be)
|
||||||
|
|
||||||
passwordTriesLeft := 1
|
passwordTriesLeft := 1
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -55,7 +54,7 @@ func walkDir(dir string) <-chan *dirEntry {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// first element is root
|
// first element is root
|
||||||
<-ch
|
_ = <-ch
|
||||||
|
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
@@ -73,16 +72,27 @@ func sameModTime(fi1, fi2 os.FileInfo) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fi1.ModTime().Equal(fi2.ModTime())
|
same := fi1.ModTime().Equal(fi2.ModTime())
|
||||||
|
if !same && (runtime.GOOS == "darwin" || runtime.GOOS == "openbsd") {
|
||||||
|
// Allow up to 1μs difference, because macOS <10.13 cannot restore
|
||||||
|
// with nanosecond precision and the current version of Go (1.9.2)
|
||||||
|
// does not yet support the new syscall. (#1087)
|
||||||
|
mt1 := fi1.ModTime()
|
||||||
|
mt2 := fi2.ModTime()
|
||||||
|
usecDiff := (mt1.Nanosecond()-mt2.Nanosecond())/1000 + (mt1.Second()-mt2.Second())*1000000
|
||||||
|
same = usecDiff <= 1 && usecDiff >= -1
|
||||||
|
}
|
||||||
|
return same
|
||||||
}
|
}
|
||||||
|
|
||||||
// directoriesContentsDiff returns a diff between both directories. If these
|
// directoriesEqualContents checks if both directories contain exactly the same
|
||||||
// contain exactly the same contents, then the diff is an empty string.
|
// contents.
|
||||||
func directoriesContentsDiff(dir1, dir2 string) string {
|
func directoriesEqualContents(dir1, dir2 string) bool {
|
||||||
var out bytes.Buffer
|
|
||||||
ch1 := walkDir(dir1)
|
ch1 := walkDir(dir1)
|
||||||
ch2 := walkDir(dir2)
|
ch2 := walkDir(dir2)
|
||||||
|
|
||||||
|
changes := false
|
||||||
|
|
||||||
var a, b *dirEntry
|
var a, b *dirEntry
|
||||||
for {
|
for {
|
||||||
var ok bool
|
var ok bool
|
||||||
@@ -106,27 +116,36 @@ func directoriesContentsDiff(dir1, dir2 string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ch1 == nil {
|
if ch1 == nil {
|
||||||
fmt.Fprintf(&out, "+%v\n", b.path)
|
fmt.Printf("+%v\n", b.path)
|
||||||
|
changes = true
|
||||||
} else if ch2 == nil {
|
} else if ch2 == nil {
|
||||||
fmt.Fprintf(&out, "-%v\n", a.path)
|
fmt.Printf("-%v\n", a.path)
|
||||||
} else if !a.equals(&out, b) {
|
changes = true
|
||||||
|
} else if !a.equals(b) {
|
||||||
if a.path < b.path {
|
if a.path < b.path {
|
||||||
fmt.Fprintf(&out, "-%v\n", a.path)
|
fmt.Printf("-%v\n", a.path)
|
||||||
|
changes = true
|
||||||
a = nil
|
a = nil
|
||||||
continue
|
continue
|
||||||
} else if a.path > b.path {
|
} else if a.path > b.path {
|
||||||
fmt.Fprintf(&out, "+%v\n", b.path)
|
fmt.Printf("+%v\n", b.path)
|
||||||
|
changes = true
|
||||||
b = nil
|
b = nil
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(&out, "%%%v\n", a.path)
|
fmt.Printf("%%%v\n", a.path)
|
||||||
|
changes = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a, b = nil, nil
|
a, b = nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return out.String()
|
if changes {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
type dirStat struct {
|
type dirStat struct {
|
||||||
|
|||||||
@@ -4,26 +4,25 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *dirEntry) equals(out io.Writer, other *dirEntry) bool {
|
func (e *dirEntry) equals(other *dirEntry) bool {
|
||||||
if e.path != other.path {
|
if e.path != other.path {
|
||||||
fmt.Fprintf(out, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path)
|
fmt.Fprintf(os.Stderr, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.fi.Mode() != other.fi.Mode() {
|
if e.fi.Mode() != other.fi.Mode() {
|
||||||
fmt.Fprintf(out, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode())
|
fmt.Fprintf(os.Stderr, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sameModTime(e.fi, other.fi) {
|
if !sameModTime(e.fi, other.fi) {
|
||||||
fmt.Fprintf(out, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime())
|
fmt.Fprintf(os.Stderr, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,17 +30,17 @@ func (e *dirEntry) equals(out io.Writer, other *dirEntry) bool {
|
|||||||
stat2, _ := other.fi.Sys().(*syscall.Stat_t)
|
stat2, _ := other.fi.Sys().(*syscall.Stat_t)
|
||||||
|
|
||||||
if stat.Uid != stat2.Uid {
|
if stat.Uid != stat2.Uid {
|
||||||
fmt.Fprintf(out, "%v: UID does not match (%v != %v)\n", e.path, stat.Uid, stat2.Uid)
|
fmt.Fprintf(os.Stderr, "%v: UID does not match (%v != %v)\n", e.path, stat.Uid, stat2.Uid)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if stat.Gid != stat2.Gid {
|
if stat.Gid != stat2.Gid {
|
||||||
fmt.Fprintf(out, "%v: GID does not match (%v != %v)\n", e.path, stat.Gid, stat2.Gid)
|
fmt.Fprintf(os.Stderr, "%v: GID does not match (%v != %v)\n", e.path, stat.Gid, stat2.Gid)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if stat.Nlink != stat2.Nlink {
|
if stat.Nlink != stat2.Nlink {
|
||||||
fmt.Fprintf(out, "%v: Number of links do not match (%v != %v)\n", e.path, stat.Nlink, stat2.Nlink)
|
fmt.Fprintf(os.Stderr, "%v: Number of links do not match (%v != %v)\n", e.path, stat.Nlink, stat2.Nlink)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,24 +4,23 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *dirEntry) equals(out io.Writer, other *dirEntry) bool {
|
func (e *dirEntry) equals(other *dirEntry) bool {
|
||||||
if e.path != other.path {
|
if e.path != other.path {
|
||||||
fmt.Fprintf(out, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path)
|
fmt.Fprintf(os.Stderr, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.fi.Mode() != other.fi.Mode() {
|
if e.fi.Mode() != other.fi.Mode() {
|
||||||
fmt.Fprintf(out, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode())
|
fmt.Fprintf(os.Stderr, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sameModTime(e.fi, other.fi) {
|
if !sameModTime(e.fi, other.fi) {
|
||||||
fmt.Fprintf(out, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime())
|
fmt.Fprintf(os.Stderr, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+85
-474
@@ -13,7 +13,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -51,11 +50,11 @@ func testRunInit(t testing.TB, opts GlobalOptions) {
|
|||||||
restic.TestDisableCheckPolynomial(t)
|
restic.TestDisableCheckPolynomial(t)
|
||||||
restic.TestSetLockTimeout(t, 0)
|
restic.TestSetLockTimeout(t, 0)
|
||||||
|
|
||||||
rtest.OK(t, runInit(InitOptions{}, opts, nil))
|
rtest.OK(t, runInit(opts, nil))
|
||||||
t.Logf("repository initialized at %v", opts.Repo)
|
t.Logf("repository initialized at %v", opts.Repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) error {
|
func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) {
|
||||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
backupErr := runBackup(opts, gopts, term, target)
|
rtest.OK(t, runBackup(opts, gopts, term, target))
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
@@ -78,13 +77,6 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return backupErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) {
|
|
||||||
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
|
|
||||||
rtest.Assert(t, err == nil, "Error while backing up")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs {
|
func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs {
|
||||||
@@ -151,21 +143,6 @@ func testRunCheckOutput(gopts GlobalOptions) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := runCheck(opts, gopts, nil)
|
err := runCheck(opts, gopts, nil)
|
||||||
return buf.String(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRunDiffOutput(gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) {
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
globalOptions.stdout = buf
|
|
||||||
defer func() {
|
|
||||||
globalOptions.stdout = os.Stdout
|
|
||||||
}()
|
|
||||||
|
|
||||||
opts := DiffOptions{
|
|
||||||
ShowMetadata: false,
|
|
||||||
}
|
|
||||||
err := runDiff(opts, gopts, []string{firstSnapshotID, secondSnapshotID})
|
|
||||||
return string(buf.Bytes()), err
|
return string(buf.Bytes()), err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +169,7 @@ func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string {
|
|||||||
|
|
||||||
rtest.OK(t, runLs(opts, gopts, []string{snapshotID}))
|
rtest.OK(t, runLs(opts, gopts, []string{snapshotID}))
|
||||||
|
|
||||||
return strings.Split(buf.String(), "\n")
|
return strings.Split(string(buf.Bytes()), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunFind(t testing.TB, wantJSON bool, gopts GlobalOptions, pattern string) []byte {
|
func testRunFind(t testing.TB, wantJSON bool, gopts GlobalOptions, pattern string) []byte {
|
||||||
@@ -268,24 +245,29 @@ func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
|
|||||||
"Expected 1 snapshot to be kept, got %v", len(forgets[0].Keep))
|
"Expected 1 snapshot to be kept, got %v", len(forgets[0].Keep))
|
||||||
rtest.Assert(t, len(forgets[0].Remove) == 2,
|
rtest.Assert(t, len(forgets[0].Remove) == 2,
|
||||||
"Expected 2 snapshots to be removed, got %v", len(forgets[0].Remove))
|
"Expected 2 snapshots to be removed, got %v", len(forgets[0].Remove))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunPrune(t testing.TB, gopts GlobalOptions) {
|
func testRunPrune(t testing.TB, gopts GlobalOptions) {
|
||||||
rtest.OK(t, runPrune(gopts))
|
rtest.OK(t, runPrune(gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSetupBackupData(t testing.TB, env *testEnvironment) string {
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
return datafile
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBackup(t *testing.T) {
|
func TestBackup(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
fd, err := os.Open(datafile)
|
||||||
|
if os.IsNotExist(errors.Cause(err)) {
|
||||||
|
t.Skipf("unable to find data file %q, skipping", datafile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rtest.OK(t, err)
|
||||||
|
rtest.OK(t, fd.Close())
|
||||||
|
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
// first backup
|
// first backup
|
||||||
@@ -327,9 +309,9 @@ func TestBackup(t *testing.T) {
|
|||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
testRunRestore(t, env.gopts, restoredir, snapshotIDs[0])
|
||||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")),
|
||||||
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
|
"directories are not equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
@@ -339,7 +321,18 @@ func TestBackupNonExistingFile(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
fd, err := os.Open(datafile)
|
||||||
|
if os.IsNotExist(errors.Cause(err)) {
|
||||||
|
t.Skipf("unable to find data file %q, skipping", datafile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rtest.OK(t, err)
|
||||||
|
rtest.OK(t, fd.Close())
|
||||||
|
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
|
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
globalOptions.stderr = ioutil.Discard
|
globalOptions.stderr = ioutil.Discard
|
||||||
defer func() {
|
defer func() {
|
||||||
globalOptions.stderr = os.Stderr
|
globalOptions.stderr = os.Stderr
|
||||||
@@ -358,58 +351,6 @@ func TestBackupNonExistingFile(t *testing.T) {
|
|||||||
testRunBackup(t, "", dirs, opts, env.gopts)
|
testRunBackup(t, "", dirs, opts, env.gopts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeDataPacksExcept(gopts GlobalOptions, t *testing.T, keep restic.IDSet) {
|
|
||||||
r, err := OpenRepository(gopts)
|
|
||||||
rtest.OK(t, err)
|
|
||||||
|
|
||||||
// Get all tree packs
|
|
||||||
rtest.OK(t, r.LoadIndex(gopts.ctx))
|
|
||||||
treePacks := restic.NewIDSet()
|
|
||||||
for _, idx := range r.Index().(*repository.MasterIndex).All() {
|
|
||||||
for _, id := range idx.TreePacks() {
|
|
||||||
treePacks.Insert(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all packs containing data blobs
|
|
||||||
rtest.OK(t, r.List(gopts.ctx, restic.PackFile, func(id restic.ID, size int64) error {
|
|
||||||
if treePacks.Has(id) || keep.Has(id) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return r.Backend().Remove(gopts.ctx, restic.Handle{Type: restic.PackFile, Name: id.String()})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBackupSelfHealing(t *testing.T) {
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
|
|
||||||
p := filepath.Join(env.testdata, "test/test")
|
|
||||||
rtest.OK(t, os.MkdirAll(filepath.Dir(p), 0755))
|
|
||||||
rtest.OK(t, appendRandomData(p, 5))
|
|
||||||
|
|
||||||
opts := BackupOptions{}
|
|
||||||
|
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts)
|
|
||||||
testRunCheck(t, env.gopts)
|
|
||||||
|
|
||||||
// remove all data packs
|
|
||||||
removeDataPacksExcept(env.gopts, t, restic.NewIDSet())
|
|
||||||
|
|
||||||
testRunRebuildIndex(t, env.gopts)
|
|
||||||
// now the repo is also missing the data blob in the index; check should report this
|
|
||||||
rtest.Assert(t, runCheck(CheckOptions{}, env.gopts, nil) != nil,
|
|
||||||
"check should have reported an error")
|
|
||||||
|
|
||||||
// second backup should report an error but "heal" this situation
|
|
||||||
err := testRunBackupAssumeFailure(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts)
|
|
||||||
rtest.Assert(t, err != nil,
|
|
||||||
"backup should have reported an error")
|
|
||||||
testRunCheck(t, env.gopts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func includes(haystack []string, needle string) bool {
|
func includes(haystack []string, needle string) bool {
|
||||||
for _, s := range haystack {
|
for _, s := range haystack {
|
||||||
if s == needle {
|
if s == needle {
|
||||||
@@ -464,7 +405,7 @@ func TestBackupExclude(t *testing.T) {
|
|||||||
f, err := os.Create(fp)
|
f, err := os.Create(fp)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
fmt.Fprint(f, filename)
|
fmt.Fprintf(f, filename)
|
||||||
rtest.OK(t, f.Close())
|
rtest.OK(t, f.Close())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,32 +436,6 @@ func TestBackupExclude(t *testing.T) {
|
|||||||
"expected file %q not in first snapshot, but it's included", "passwords.txt")
|
"expected file %q not in first snapshot, but it's included", "passwords.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackupErrors(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
|
||||||
|
|
||||||
// Assume failure
|
|
||||||
inaccessibleFile := filepath.Join(env.testdata, "0", "0", "9", "0")
|
|
||||||
os.Chmod(inaccessibleFile, 0000)
|
|
||||||
defer func() {
|
|
||||||
os.Chmod(inaccessibleFile, 0644)
|
|
||||||
}()
|
|
||||||
opts := BackupOptions{}
|
|
||||||
gopts := env.gopts
|
|
||||||
gopts.stderr = ioutil.Discard
|
|
||||||
err := testRunBackupAssumeFailure(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, gopts)
|
|
||||||
rtest.Assert(t, err != nil, "Assumed failure, but no error occured.")
|
|
||||||
rtest.Assert(t, err == ErrInvalidSourceData, "Wrong error returned")
|
|
||||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
|
||||||
rtest.Assert(t, len(snapshotIDs) == 1,
|
|
||||||
"expected one snapshot, got %v", snapshotIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
incrementalFirstWrite = 10 * 1042 * 1024
|
incrementalFirstWrite = 10 * 1042 * 1024
|
||||||
incrementalSecondWrite = 1 * 1042 * 1024
|
incrementalSecondWrite = 1 * 1042 * 1024
|
||||||
@@ -591,7 +506,10 @@ func TestBackupTags(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
@@ -614,153 +532,6 @@ func TestBackupTags(t *testing.T) {
|
|||||||
"expected parent to be %v, got %v", parent.ID, newest.Parent)
|
"expected parent to be %v, got %v", parent.ID, newest.Parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) {
|
|
||||||
copyOpts := CopyOptions{
|
|
||||||
secondaryRepoOptions: secondaryRepoOptions{
|
|
||||||
Repo: dstGopts.Repo,
|
|
||||||
password: dstGopts.password,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
rtest.OK(t, runCopy(copyOpts, srcGopts, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopy(t *testing.T) {
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
env2, cleanup2 := withTestEnvironment(t)
|
|
||||||
defer cleanup2()
|
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
|
||||||
opts := BackupOptions{}
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts)
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts)
|
|
||||||
testRunCheck(t, env.gopts)
|
|
||||||
|
|
||||||
testRunInit(t, env2.gopts)
|
|
||||||
testRunCopy(t, env.gopts, env2.gopts)
|
|
||||||
|
|
||||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
|
||||||
copiedSnapshotIDs := testRunList(t, "snapshots", env2.gopts)
|
|
||||||
|
|
||||||
// Check that the copies size seems reasonable
|
|
||||||
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "expected %v snapshots, found %v",
|
|
||||||
len(snapshotIDs), len(copiedSnapshotIDs))
|
|
||||||
stat := dirStats(env.repo)
|
|
||||||
stat2 := dirStats(env2.repo)
|
|
||||||
sizeDiff := int64(stat.size) - int64(stat2.size)
|
|
||||||
if sizeDiff < 0 {
|
|
||||||
sizeDiff = -sizeDiff
|
|
||||||
}
|
|
||||||
rtest.Assert(t, sizeDiff < int64(stat.size)/50, "expected less than 2%% size difference: %v vs. %v",
|
|
||||||
stat.size, stat2.size)
|
|
||||||
|
|
||||||
// Check integrity of the copy
|
|
||||||
testRunCheck(t, env2.gopts)
|
|
||||||
|
|
||||||
// Check that the copied snapshots have the same tree contents as the old ones (= identical tree hash)
|
|
||||||
origRestores := make(map[string]struct{})
|
|
||||||
for i, snapshotID := range snapshotIDs {
|
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
|
||||||
origRestores[restoredir] = struct{}{}
|
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
|
||||||
}
|
|
||||||
for i, snapshotID := range copiedSnapshotIDs {
|
|
||||||
restoredir := filepath.Join(env2.base, fmt.Sprintf("restore%d", i))
|
|
||||||
testRunRestore(t, env2.gopts, restoredir, snapshotID)
|
|
||||||
foundMatch := false
|
|
||||||
for cmpdir := range origRestores {
|
|
||||||
diff := directoriesContentsDiff(restoredir, cmpdir)
|
|
||||||
if diff == "" {
|
|
||||||
delete(origRestores, cmpdir)
|
|
||||||
foundMatch = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rtest.Assert(t, foundMatch, "found no counterpart for snapshot %v", snapshotID)
|
|
||||||
}
|
|
||||||
|
|
||||||
rtest.Assert(t, len(origRestores) == 0, "found not copied snapshots")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyIncremental(t *testing.T) {
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
env2, cleanup2 := withTestEnvironment(t)
|
|
||||||
defer cleanup2()
|
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
|
||||||
opts := BackupOptions{}
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts)
|
|
||||||
testRunCheck(t, env.gopts)
|
|
||||||
|
|
||||||
testRunInit(t, env2.gopts)
|
|
||||||
testRunCopy(t, env.gopts, env2.gopts)
|
|
||||||
|
|
||||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
|
||||||
copiedSnapshotIDs := testRunList(t, "snapshots", env2.gopts)
|
|
||||||
|
|
||||||
// Check that the copies size seems reasonable
|
|
||||||
testRunCheck(t, env2.gopts)
|
|
||||||
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "expected %v snapshots, found %v",
|
|
||||||
len(snapshotIDs), len(copiedSnapshotIDs))
|
|
||||||
|
|
||||||
// check that no snapshots are copied, as there are no new ones
|
|
||||||
testRunCopy(t, env.gopts, env2.gopts)
|
|
||||||
testRunCheck(t, env2.gopts)
|
|
||||||
copiedSnapshotIDs = testRunList(t, "snapshots", env2.gopts)
|
|
||||||
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "still expected %v snapshots, found %v",
|
|
||||||
len(snapshotIDs), len(copiedSnapshotIDs))
|
|
||||||
|
|
||||||
// check that only new snapshots are copied
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts)
|
|
||||||
testRunCopy(t, env.gopts, env2.gopts)
|
|
||||||
testRunCheck(t, env2.gopts)
|
|
||||||
snapshotIDs = testRunList(t, "snapshots", env.gopts)
|
|
||||||
copiedSnapshotIDs = testRunList(t, "snapshots", env2.gopts)
|
|
||||||
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "still expected %v snapshots, found %v",
|
|
||||||
len(snapshotIDs), len(copiedSnapshotIDs))
|
|
||||||
|
|
||||||
// also test the reverse direction
|
|
||||||
testRunCopy(t, env2.gopts, env.gopts)
|
|
||||||
testRunCheck(t, env.gopts)
|
|
||||||
snapshotIDs = testRunList(t, "snapshots", env.gopts)
|
|
||||||
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "still expected %v snapshots, found %v",
|
|
||||||
len(copiedSnapshotIDs), len(snapshotIDs))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInitCopyChunkerParams(t *testing.T) {
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
env2, cleanup2 := withTestEnvironment(t)
|
|
||||||
defer cleanup2()
|
|
||||||
|
|
||||||
testRunInit(t, env2.gopts)
|
|
||||||
|
|
||||||
initOpts := InitOptions{
|
|
||||||
secondaryRepoOptions: secondaryRepoOptions{
|
|
||||||
Repo: env2.gopts.Repo,
|
|
||||||
password: env2.gopts.password,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rtest.Assert(t, runInit(initOpts, env.gopts, nil) != nil, "expected invalid init options to fail")
|
|
||||||
|
|
||||||
initOpts.CopyChunkerParameters = true
|
|
||||||
rtest.OK(t, runInit(initOpts, env.gopts, nil))
|
|
||||||
|
|
||||||
repo, err := OpenRepository(env.gopts)
|
|
||||||
rtest.OK(t, err)
|
|
||||||
|
|
||||||
otherRepo, err := OpenRepository(env2.gopts)
|
|
||||||
rtest.OK(t, err)
|
|
||||||
|
|
||||||
rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial,
|
|
||||||
"expected equal chunker polynomials, got %v expected %v", repo.Config().ChunkerPolynomial,
|
|
||||||
otherRepo.Config().ChunkerPolynomial)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) {
|
func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) {
|
||||||
rtest.OK(t, runTag(opts, gopts, []string{}))
|
rtest.OK(t, runTag(opts, gopts, []string{}))
|
||||||
}
|
}
|
||||||
@@ -769,7 +540,10 @@ func TestTag(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
newest, _ := testRunSnapshots(t, env.gopts)
|
newest, _ := testRunSnapshots(t, env.gopts)
|
||||||
@@ -865,28 +639,6 @@ func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions)
|
|||||||
rtest.OK(t, runKey(gopts, []string{"add"}))
|
rtest.OK(t, runKey(gopts, []string{"add"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) {
|
|
||||||
testKeyNewPassword = "john's geheimnis"
|
|
||||||
defer func() {
|
|
||||||
testKeyNewPassword = ""
|
|
||||||
keyUsername = ""
|
|
||||||
keyHostname = ""
|
|
||||||
}()
|
|
||||||
|
|
||||||
cmdKey.Flags().Parse([]string{"--user=john", "--host=example.com"})
|
|
||||||
|
|
||||||
t.Log("adding key for john@example.com")
|
|
||||||
rtest.OK(t, runKey(gopts, []string{"add"}))
|
|
||||||
|
|
||||||
repo, err := OpenRepository(gopts)
|
|
||||||
rtest.OK(t, err)
|
|
||||||
key, err := repository.SearchKey(gopts.ctx, repo, testKeyNewPassword, 1, "")
|
|
||||||
rtest.OK(t, err)
|
|
||||||
|
|
||||||
rtest.Equals(t, "john", key.Username)
|
|
||||||
rtest.Equals(t, "example.com", key.Hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
|
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
|
||||||
testKeyNewPassword = newPassword
|
testKeyNewPassword = newPassword
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -929,8 +681,6 @@ func TestKeyAddRemove(t *testing.T) {
|
|||||||
t.Logf("testing access with last password %q\n", env.gopts.password)
|
t.Logf("testing access with last password %q\n", env.gopts.password)
|
||||||
rtest.OK(t, runKey(env.gopts, []string{"list"}))
|
rtest.OK(t, runKey(env.gopts, []string{"list"}))
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
|
|
||||||
testRunKeyAddNewKeyUserHost(t, env.gopts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFileSize(filename string, size int64) error {
|
func testFileSize(filename string, size int64) error {
|
||||||
@@ -1017,8 +767,8 @@ func TestRestore(t *testing.T) {
|
|||||||
restoredir := filepath.Join(env.base, "restore")
|
restoredir := filepath.Join(env.base, "restore")
|
||||||
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
|
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
|
||||||
|
|
||||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata)))
|
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata))),
|
||||||
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
"directories are not equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRestoreLatest(t *testing.T) {
|
func TestRestoreLatest(t *testing.T) {
|
||||||
@@ -1151,14 +901,14 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) {
|
|||||||
testRunRestoreIncludes(t, env.gopts, filepath.Join(env.base, "restore0"), snapshotID, []string{"*.ext"})
|
testRunRestoreIncludes(t, env.gopts, filepath.Join(env.base, "restore0"), snapshotID, []string{"*.ext"})
|
||||||
|
|
||||||
f1 := filepath.Join(env.base, "restore0", "testdata", "subdir1", "subdir2")
|
f1 := filepath.Join(env.base, "restore0", "testdata", "subdir1", "subdir2")
|
||||||
_, err := os.Stat(f1)
|
fi, err := os.Stat(f1)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
// restore with filter "*", this should restore meta data on everything.
|
// restore with filter "*", this should restore meta data on everything.
|
||||||
testRunRestoreIncludes(t, env.gopts, filepath.Join(env.base, "restore1"), snapshotID, []string{"*"})
|
testRunRestoreIncludes(t, env.gopts, filepath.Join(env.base, "restore1"), snapshotID, []string{"*"})
|
||||||
|
|
||||||
f2 := filepath.Join(env.base, "restore1", "testdata", "subdir1", "subdir2")
|
f2 := filepath.Join(env.base, "restore1", "testdata", "subdir1", "subdir2")
|
||||||
fi, err := os.Stat(f2)
|
fi, err = os.Stat(f2)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Assert(t, fi.ModTime() == time.Unix(0, 0),
|
rtest.Assert(t, fi.ModTime() == time.Unix(0, 0),
|
||||||
@@ -1169,7 +919,10 @@ func TestFind(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := testSetupBackupData(t, env)
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
@@ -1206,7 +959,10 @@ func TestFindJSON(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := testSetupBackupData(t, env)
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
@@ -1267,37 +1023,6 @@ func TestRebuildIndexAlwaysFull(t *testing.T) {
|
|||||||
TestRebuildIndex(t)
|
TestRebuildIndex(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
type appendOnlyBackend struct {
|
|
||||||
restic.Backend
|
|
||||||
}
|
|
||||||
|
|
||||||
// called via repo.Backend().Remove()
|
|
||||||
func (b *appendOnlyBackend) Remove(ctx context.Context, h restic.Handle) error {
|
|
||||||
return errors.Errorf("Failed to remove %v", h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
|
|
||||||
rtest.SetupTarTestFixture(t, env.base, datafile)
|
|
||||||
|
|
||||||
globalOptions.stdout = ioutil.Discard
|
|
||||||
defer func() {
|
|
||||||
globalOptions.stdout = os.Stdout
|
|
||||||
}()
|
|
||||||
|
|
||||||
env.gopts.backendTestHook = func(r restic.Backend) (restic.Backend, error) {
|
|
||||||
return &appendOnlyBackend{r}, nil
|
|
||||||
}
|
|
||||||
err := runRebuildIndex(env.gopts)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected rebuildIndex to fail")
|
|
||||||
}
|
|
||||||
t.Log(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckRestoreNoLock(t *testing.T) {
|
func TestCheckRestoreNoLock(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -1329,7 +1054,18 @@ func TestPrune(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
fd, err := os.Open(datafile)
|
||||||
|
if os.IsNotExist(errors.Cause(err)) {
|
||||||
|
t.Skipf("unable to find data file %q, skipping", datafile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rtest.OK(t, err)
|
||||||
|
rtest.OK(t, fd.Close())
|
||||||
|
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
||||||
@@ -1350,58 +1086,6 @@ func TestPrune(t *testing.T) {
|
|||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listPacks(gopts GlobalOptions, t *testing.T) restic.IDSet {
|
|
||||||
r, err := OpenRepository(gopts)
|
|
||||||
rtest.OK(t, err)
|
|
||||||
|
|
||||||
packs := restic.NewIDSet()
|
|
||||||
|
|
||||||
rtest.OK(t, r.List(gopts.ctx, restic.PackFile, func(id restic.ID, size int64) error {
|
|
||||||
packs.Insert(id)
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
return packs
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPruneWithDamagedRepository(t *testing.T) {
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
opts := BackupOptions{}
|
|
||||||
|
|
||||||
// create and delete snapshot to create unused blobs
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts)
|
|
||||||
firstSnapshot := testRunList(t, "snapshots", env.gopts)
|
|
||||||
rtest.Assert(t, len(firstSnapshot) == 1,
|
|
||||||
"expected one snapshot, got %v", firstSnapshot)
|
|
||||||
testRunForget(t, env.gopts, firstSnapshot[0].String())
|
|
||||||
|
|
||||||
oldPacks := listPacks(env.gopts, t)
|
|
||||||
|
|
||||||
// create new snapshot, but lose all data
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts)
|
|
||||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
|
||||||
|
|
||||||
removeDataPacksExcept(env.gopts, t, oldPacks)
|
|
||||||
|
|
||||||
rtest.Assert(t, len(snapshotIDs) == 1,
|
|
||||||
"expected one snapshot, got %v", snapshotIDs)
|
|
||||||
|
|
||||||
// prune should fail
|
|
||||||
err := runPrune(env.gopts)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected prune to fail")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "blobs seem to be missing") {
|
|
||||||
t.Fatalf("did not find hint for missing blobs")
|
|
||||||
}
|
|
||||||
t.Log(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHardLink(t *testing.T) {
|
func TestHardLink(t *testing.T) {
|
||||||
// this test assumes a test set with a single directory containing hard linked files
|
// this test assumes a test set with a single directory containing hard linked files
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
@@ -1436,9 +1120,9 @@ func TestHardLink(t *testing.T) {
|
|||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
testRunRestore(t, env.gopts, restoredir, snapshotIDs[0])
|
||||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")),
|
||||||
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
"directories are not equal")
|
||||||
|
|
||||||
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
|
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
|
||||||
rtest.Assert(t, linksEqual(linkTests, linkResults),
|
rtest.Assert(t, linksEqual(linkTests, linkResults),
|
||||||
@@ -1463,7 +1147,11 @@ func linksEqual(source, dest map[uint64][]string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return len(dest) == 0
|
if len(dest) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkEqual(source, dest []string) bool {
|
func linkEqual(source, dest []string) bool {
|
||||||
@@ -1500,7 +1188,18 @@ func TestQuietBackup(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
fd, err := os.Open(datafile)
|
||||||
|
if os.IsNotExist(errors.Cause(err)) {
|
||||||
|
t.Skipf("unable to find data file %q, skipping", datafile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rtest.OK(t, err)
|
||||||
|
rtest.OK(t, fd.Close())
|
||||||
|
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
env.gopts.Quiet = false
|
env.gopts.Quiet = false
|
||||||
@@ -1519,91 +1218,3 @@ func TestQuietBackup(t *testing.T) {
|
|||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(dst string, src string) error {
|
|
||||||
srcFile, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer srcFile.Close()
|
|
||||||
|
|
||||||
dstFile, err := os.Create(dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dstFile.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(dstFile, srcFile)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var diffOutputRegexPatterns = []string{
|
|
||||||
"-.+modfile",
|
|
||||||
"M.+modfile1",
|
|
||||||
"\\+.+modfile2",
|
|
||||||
"\\+.+modfile3",
|
|
||||||
"\\+.+modfile4",
|
|
||||||
"-.+submoddir",
|
|
||||||
"-.+submoddir.subsubmoddir",
|
|
||||||
"\\+.+submoddir2",
|
|
||||||
"\\+.+submoddir2.subsubmoddir",
|
|
||||||
"Files: +2 new, +1 removed, +1 changed",
|
|
||||||
"Dirs: +3 new, +2 removed",
|
|
||||||
"Data Blobs: +2 new, +1 removed",
|
|
||||||
"Added: +7[0-9]{2}\\.[0-9]{3} KiB",
|
|
||||||
"Removed: +2[0-9]{2}\\.[0-9]{3} KiB",
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiff(t *testing.T) {
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
|
|
||||||
datadir := filepath.Join(env.base, "testdata")
|
|
||||||
testdir := filepath.Join(datadir, "testdir")
|
|
||||||
subtestdir := filepath.Join(testdir, "subtestdir")
|
|
||||||
testfile := filepath.Join(testdir, "testfile")
|
|
||||||
|
|
||||||
rtest.OK(t, os.Mkdir(testdir, 0755))
|
|
||||||
rtest.OK(t, os.Mkdir(subtestdir, 0755))
|
|
||||||
rtest.OK(t, appendRandomData(testfile, 256*1024))
|
|
||||||
|
|
||||||
moddir := filepath.Join(datadir, "moddir")
|
|
||||||
submoddir := filepath.Join(moddir, "submoddir")
|
|
||||||
subsubmoddir := filepath.Join(submoddir, "subsubmoddir")
|
|
||||||
modfile := filepath.Join(moddir, "modfile")
|
|
||||||
rtest.OK(t, os.Mkdir(moddir, 0755))
|
|
||||||
rtest.OK(t, os.Mkdir(submoddir, 0755))
|
|
||||||
rtest.OK(t, os.Mkdir(subsubmoddir, 0755))
|
|
||||||
rtest.OK(t, copyFile(modfile, testfile))
|
|
||||||
rtest.OK(t, appendRandomData(modfile+"1", 256*1024))
|
|
||||||
|
|
||||||
snapshots := make(map[string]struct{})
|
|
||||||
opts := BackupOptions{}
|
|
||||||
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
|
||||||
snapshots, firstSnapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts))
|
|
||||||
|
|
||||||
rtest.OK(t, os.Rename(modfile, modfile+"3"))
|
|
||||||
rtest.OK(t, os.Rename(submoddir, submoddir+"2"))
|
|
||||||
rtest.OK(t, appendRandomData(modfile+"1", 256*1024))
|
|
||||||
rtest.OK(t, appendRandomData(modfile+"2", 256*1024))
|
|
||||||
rtest.OK(t, os.Mkdir(modfile+"4", 0755))
|
|
||||||
|
|
||||||
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
|
||||||
snapshots, secondSnapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts))
|
|
||||||
|
|
||||||
_, err := testRunDiffOutput(env.gopts, "", secondSnapshotID)
|
|
||||||
rtest.Assert(t, err != nil, "expected error on invalid snapshot id")
|
|
||||||
|
|
||||||
out, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error from diff for test repository, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pattern := range diffOutputRegexPatterns {
|
|
||||||
r, err := regexp.Compile(pattern)
|
|
||||||
rtest.Assert(t, err == nil, "failed to compile regexp %v", pattern)
|
|
||||||
rtest.Assert(t, r.MatchString(out), "expected pattern %v in output, got\n%v", pattern, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 247 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user