Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Neumann
7e72d638df Add patched chunker
Referenced in https://forum.restic.net/t/restic-slice-bounds-out-of-range/2617/20
2020-04-24 20:56:45 +02:00
247 changed files with 4535 additions and 7980 deletions

View File

@@ -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

View File

@@ -10,11 +10,11 @@ your time and add more commits. If you're done and ready for review, please
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?
@@ -23,8 +23,8 @@ Was the change discussed in an issue or in the forum before?
<!--
Link issues and relevant forum posts here.
If this PR resolves an issue on GitHub, use "Closes #1234" so that the issue
is closed automatically when this PR is merged.
If this PR resolves an issue on GitHub, use "closes #1234" so that the issue is
closed automatically when this PR is merged.
-->
Checklist

View File

@@ -3,6 +3,22 @@ sudo: false
matrix:
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
go: "1.13.x"
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
@@ -11,17 +27,9 @@ matrix:
- $HOME/.cache/go-build
- $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
- os: linux
go: "1.15.x"
go: "1.14.x"
sudo: true
cache:
directories:
@@ -29,7 +37,7 @@ matrix:
- $HOME/gopath/pkg/mod
- os: osx
go: "1.15.x"
go: "1.14.x"
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
cache:
directories:

View File

@@ -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)
=======================================
@@ -1825,10 +1361,10 @@ Details
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
to create files with arbitrary 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
restoring a snapshot containing this file on Windows, it would be written to the parent of the
target directory.
to create create files with arbitrary 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 restoring a snapshot containing this file on Windows, it would be written to the parent of
the target directory.
We'd like to thank Tyler Spivey for reporting this responsibly!

View File

@@ -60,11 +60,16 @@ uploading it somewhere or post only the parts that are really relevant.
Development Environment
=======================
The repository contains the code written for restic in the directories
`cmd/` and `internal/`.
The repository contains several sets of directories with code: `cmd/` and
`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
having `$GOPATH` set) and `cd` into the directory:
Go >= 1.11
----------
For Go version 1.11 or later, you should clone the repo (without having
`$GOPATH` set) and `cd` into the directory:
$ unset GOPATH
$ git clone https://github.com/restic/restic
@@ -74,12 +79,40 @@ Then use the `go` tool to build restic:
$ go build ./cmd/restic
$ ./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:
$ 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
=================
@@ -92,14 +125,15 @@ down to the following steps:
GitHub. For a new feature, please add an issue before starting to work on
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
your changes. If you are working on the code itself, please set up the
development environment as described in the previous section.
2. Clone the repository locally and create a new branch. If you are working on
the code itself, please set up the development environment as described in
the previous section.
3. Commit your changes to the new branch as fine grained as possible, as
smaller patches, for individual changes, are easier to discuss and merge.
3. Then commit your changes as fine grained as possible, as smaller patches,
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.
@@ -112,19 +146,20 @@ down to the following steps:
existing commit, use common sense to decide which is better), they will be
automatically added to the pull request.
7. If your pull request changes anything that users should be aware of
(a bugfix, a new feature, ...) please add an entry as a new file in
`changelog/unreleased` including the issue number in the filename (e.g.
`issue-8756`). Use the template in `changelog/TEMPLATE` for the content.
It will be used in the announcement of the next stable release. While
writing, ask yourself: If I were the user, what would I need to be aware
of with this change?
7. If your pull request changes anything that users should be aware
of (a bugfix, a new feature, ...) please add an entry as a new
file in `changelog/unreleased` including the issue number in the
filename (e.g. `issue-8756`). Use the template in
`changelog/TEMPLATE` for the content. It will be used in the
announcement of the next stable release. While writing, ask
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
a lot for your contribution!
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
run

View File

@@ -1 +1 @@
0.10.0
0.9.6

View File

@@ -20,8 +20,8 @@ init:
install:
- rmdir c:\go /s /q
- appveyor DownloadFile https://dl.google.com/go/go1.15.2.windows-amd64.msi
- msiexec /i go1.15.2.windows-amd64.msi /q
- appveyor DownloadFile https://dl.google.com/go/go1.14.windows-amd64.msi
- msiexec /i go1.14.windows-amd64.msi /q
- go version
- go env
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip

View File

@@ -3,7 +3,7 @@
// 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.
//
// 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.
// BSD 2-Clause License
@@ -327,8 +327,8 @@ func (v GoVersion) String() string {
}
func main() {
if !goVersion.AtLeast(GoVersion{1, 12, 0}) {
die("Go version (%v) is too old, restic requires Go >= 1.12\n", goVersion)
if !goVersion.AtLeast(GoVersion{1, 11, 0}) {
die("Go version (%v) is too old, Go <= 1.11 does not support Go Modules\n", goVersion)
}
if !goVersion.AtLeast(config.MinVersion) {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
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
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

View File

@@ -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
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

View File

@@ -1,9 +1,7 @@
Enhancement: 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
blocks. It should improve the chunking throughput by 5-10% depending on the
CPU.
https://github.com/restic/restic/pull/2576
https://github.com/restic/restic/pull/2845
https://github.com/restic/restic/issues/2820

View File

@@ -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

25
chunker/.travis.yml Normal file
View File

@@ -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 "")

23
chunker/LICENSE Normal file
View File

@@ -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.

12
chunker/README.md Normal file
View File

@@ -0,0 +1,12 @@
[![GoDoc](https://godoc.org/github.com/restic/chunker?status.svg)](http://godoc.org/github.com/restic/chunker)
[![Build Status](https://travis-ci.com/restic/chunker.svg?branch=master)](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

383
chunker/chunker.go Normal file
View File

@@ -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)
}

347
chunker/chunker_test.go Normal file
View File

@@ -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)
}
}

82
chunker/doc.go Normal file
View File

@@ -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

39
chunker/example_test.go Normal file
View File

@@ -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
}

3
chunker/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/restic/chunker
go 1.14

310
chunker/polynomials.go Normal file
View File

@@ -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
}

424
chunker/polynomials_test.go Normal file
View File

@@ -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)
}
}
}

View File

@@ -1,4 +1,4 @@
package dump
package main
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
@@ -35,6 +35,17 @@ type aclElement struct {
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 {
return int(a >> 32)
}

View File

@@ -1,4 +1,4 @@
package dump
package main
import (
"reflect"

View File

@@ -1,6 +1,7 @@
package main
import (
"fmt"
"os"
"os/signal"
"sync"
@@ -16,6 +17,8 @@ var cleanupHandlers struct {
ch chan os.Signal
}
var stderr = os.Stderr
func init() {
cleanupHandlers.ch = make(chan os.Signal, 1)
go CleanupHandler(cleanupHandlers.ch)
@@ -48,7 +51,7 @@ func RunCleanupHandlers() {
for _, f := range cleanupHandlers.list {
err := f()
if err != nil {
Warnf("error in cleanup handler: %v\n", err)
fmt.Fprintf(stderr, "error in cleanup handler: %v\n", err)
}
}
cleanupHandlers.list = nil
@@ -58,7 +61,7 @@ func RunCleanupHandlers() {
func CleanupHandler(c <-chan os.Signal) {
for s := range c {
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

View File

@@ -39,9 +39,10 @@ given as the arguments.
EXIT STATUS
===========
Exit status is 0 if the command was successful.
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).
Exit status is 0 if the command was successful, and non-zero if there was any error.
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) {
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.
type BackupOptions struct {
Parent string
Force bool
Excludes []string
InsensitiveExcludes []string
ExcludeFiles []string
InsensitiveExcludeFiles []string
ExcludeOtherFS bool
ExcludeIfPresent []string
ExcludeCaches bool
ExcludeLargerThan string
Stdin bool
StdinFilename string
Tags []string
Host string
FilesFrom []string
TimeStamp string
WithAtime bool
IgnoreInode bool
Parent string
Force bool
Excludes []string
InsensitiveExcludes []string
ExcludeFiles []string
ExcludeOtherFS bool
ExcludeIfPresent []string
ExcludeCaches bool
Stdin bool
StdinFilename string
Tags []string
Host string
FilesFrom []string
TimeStamp string
WithAtime bool
IgnoreInode bool
}
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() {
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.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.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.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.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.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.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup 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)")
@@ -243,14 +237,6 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, t
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 {
fs = append(fs, rejectByInsensitivePattern(opts.InsensitiveExcludes))
}
@@ -287,14 +273,6 @@ func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets
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
}
@@ -437,7 +415,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
var t tomb.Tomb
if gopts.verbosity >= 2 && !gopts.JSON {
Verbosef("open repository\n")
term.Print("open repository\n")
}
repo, err := OpenRepository(gopts)
@@ -579,11 +557,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
arch.SelectByName = selectByNameFilter
arch.Select = selectFilter
arch.WithAtime = opts.WithAtime
success := true
arch.Error = func(item string, fi os.FileInfo, err error) error {
success = false
return p.Error(item, fi, err)
}
arch.Error = p.Error
arch.CompleteItem = p.CompleteItem
arch.StartFile = p.StartFile
arch.CompleteBlob = p.CompleteBlob
@@ -601,6 +575,24 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
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 {
p.V("start backup on %v", targets)
}
@@ -620,9 +612,6 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
if !gopts.JSON {
p.P("snapshot %s saved\n", id.Str())
}
if !success {
return ErrInvalidSourceData
}
// Return error if any
return err

View File

@@ -2,6 +2,8 @@ package main
import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
@@ -74,7 +76,7 @@ func runCat(gopts GlobalOptions, args []string) error {
return err
}
Println(string(buf))
fmt.Println(string(buf))
return nil
case "index":
buf, err := repo.LoadAndDecrypt(gopts.ctx, nil, restic.IndexFile, id)
@@ -82,8 +84,9 @@ func runCat(gopts GlobalOptions, args []string) error {
return err
}
Println(string(buf))
return nil
_, err = os.Stdout.Write(append(buf, '\n'))
return err
case "snapshot":
sn := &restic.Snapshot{}
err = repo.LoadJSONUnpacked(gopts.ctx, restic.SnapshotFile, id, sn)
@@ -96,7 +99,8 @@ func runCat(gopts GlobalOptions, args []string) error {
return err
}
Println(string(buf))
fmt.Println(string(buf))
return nil
case "key":
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
@@ -116,7 +120,7 @@ func runCat(gopts GlobalOptions, args []string) error {
return err
}
Println(string(buf))
fmt.Println(string(buf))
return nil
case "masterkey":
buf, err := json.MarshalIndent(repo.Key(), "", " ")
@@ -124,7 +128,7 @@ func runCat(gopts GlobalOptions, args []string) error {
return err
}
Println(string(buf))
fmt.Println(string(buf))
return nil
case "lock":
lock, err := restic.LoadLock(gopts.ctx, repo, id)
@@ -137,7 +141,8 @@ func runCat(gopts GlobalOptions, args []string) error {
return err
}
Println(string(buf))
fmt.Println(string(buf))
return nil
}
@@ -149,7 +154,7 @@ func runCat(gopts GlobalOptions, args []string) error {
switch tpe {
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)
if err != nil {
return err
@@ -157,15 +162,16 @@ func runCat(gopts GlobalOptions, args []string) error {
hash := restic.Hash(buf)
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
case "blob":
for _, t := range []restic.BlobType{restic.DataBlob, restic.TreeBlob} {
if !repo.Index().Has(id, t) {
_, found := repo.Index().Lookup(id, t)
if !found {
continue
}
@@ -174,7 +180,7 @@ func runCat(gopts GlobalOptions, args []string) error {
return err
}
_, err = globalOptions.stdout.Write(buf)
_, err = os.Stdout.Write(buf)
return err
}

View File

@@ -3,8 +3,10 @@ package main
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
@@ -98,6 +100,36 @@ func stringToIntSlice(param string) (split []uint, err error) {
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.
//
// * if --with-cache is specified, the default cache is used
@@ -203,7 +235,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
continue
}
errorsFound = true
Warnf("%v\n", err)
fmt.Fprintf(os.Stderr, "%v\n", err)
}
if orphanedPacks > 0 {
@@ -217,18 +249,18 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
for err := range errChan {
errorsFound = true
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 {
Warnf(" %v\n", treeErr)
fmt.Fprintf(os.Stderr, " %v\n", treeErr)
}
} else {
Warnf("error: %v\n", err)
fmt.Fprintf(os.Stderr, "error: %v\n", err)
}
}
if opts.CheckUnused {
for _, id := range chkr.UnusedBlobs() {
Verbosef("unused blob %v\n", id)
Verbosef("unused blob %v\n", id.Str())
errorsFound = true
}
}
@@ -250,14 +282,14 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
Verbosef("read all data\n")
}
p := newProgressMax(!gopts.Quiet, packCount, "packs")
p := newReadProgress(gopts, restic.Stat{Blobs: packCount})
errChan := make(chan error)
go chkr.ReadPacks(gopts.ctx, packs, p, errChan)
for err := range errChan {
errorsFound = true
Warnf("%v\n", err)
fmt.Fprintf(os.Stderr, "%v\n", err)
}
}

View File

@@ -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, &copyOptions.secondaryRepoOptions, "destination", "to copy snapshots to")
f.StringArrayVarP(&copyOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
f.Var(&copyOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given")
f.StringArrayVar(&copyOptions.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
}

View File

@@ -84,12 +84,12 @@ type Blob struct {
func printPacks(repo *repository.Repository, wr io.Writer) error {
return repo.List(context.TODO(), restic.PackFile, func(id restic.ID, size int64) error {
h := restic.Handle{Type: restic.PackFile, Name: id.String()}
return repo.List(context.TODO(), restic.DataFile, func(id restic.ID, size int64) error {
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
blobs, err := pack.List(repo.Key(), restic.ReaderAt(repo.Backend(), h), size)
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
}
@@ -112,7 +112,7 @@ func printPacks(repo *repository.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 {
Printf("index_id: %v\n", id)
fmt.Printf("index_id: %v\n", id)
idx, err := repository.LoadIndex(context.TODO(), repo, id)
if err != nil {
@@ -151,13 +151,13 @@ func runDebugDump(gopts GlobalOptions, args []string) error {
case "packs":
return printPacks(repo, gopts.stdout)
case "all":
Printf("snapshots:\n")
fmt.Printf("snapshots:\n")
err := debugPrintSnapshots(repo, gopts.stdout)
if err != nil {
return err
}
Printf("\nindexes:\n")
fmt.Printf("\nindexes:\n")
err = dumpIndexes(repo, gopts.stdout)
if err != nil {
return err

View File

@@ -14,7 +14,7 @@ import (
)
var cmdDiff = &cobra.Command{
Use: "diff [flags] snapshot-ID snapshot-ID",
Use: "diff snapshot-ID snapshot-ID",
Short: "Show differences between two snapshots",
Long: `
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.
type DiffStats struct {
ChangedFiles int
Added DiffStat
Removed DiffStat
BlobsBefore, BlobsAfter, BlobsCommon restic.BlobSet
ChangedFiles int
Added DiffStat
Removed DiffStat
BlobsBefore, BlobsAfter restic.BlobSet
}
// NewDiffStats creates new stats for a diff run.
@@ -127,7 +127,6 @@ func NewDiffStats() *DiffStats {
return &DiffStats{
BlobsBefore: 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
}
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) {
names := make(map[string]struct{})
tree1Nodes = make(map[string]*restic.Node)
@@ -218,7 +196,7 @@ func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[stri
uniqueNames = append(uniqueNames, name)
}
sort.Strings(uniqueNames)
sort.Sort(sort.StringSlice(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" {
var err error
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)
}
err := c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
if err != nil {
Warnf("error: %v\n", err)
}
@@ -372,8 +345,8 @@ func runDiff(opts DiffOptions, gopts GlobalOptions, args []string) error {
}
both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed)
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added)
updateBlobs(repo, stats.BlobsBefore.Sub(both), &stats.Removed)
updateBlobs(repo, stats.BlobsAfter.Sub(both), &stats.Added)
Printf("\n")
Printf("Files: %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)

View File

@@ -1,16 +1,19 @@
package main
import (
"archive/tar"
"context"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/dump"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/walker"
"github.com/spf13/cobra"
)
@@ -19,10 +22,8 @@ var cmdDump = &cobra.Command{
Use: "dump [flags] snapshotID file",
Short: "Print a backed-up file to stdout",
Long: `
The "dump" command extracts files from a snapshot from the repository. If a
single file is selected, it prints its contents to stdout. Folders are output
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 "dump" command extracts a single file from a snapshot from the repository and
prints its contents to stdout.
The special snapshot "latest" can be used to use the latest snapshot in the
repository.
@@ -58,14 +59,17 @@ func init() {
func splitPath(p string) []string {
d, f := path.Split(p)
if d == "" || d == "/" {
if d == "" {
return []string{f}
}
s := splitPath(path.Join("/", d))
if d == "/" {
return []string{d}
}
s := splitPath(path.Clean(d))
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 {
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 {
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])
for _, node := range tree.Nodes {
// If dumping something in the highest level it will just take the
// first item it finds and dump that according to the switch case below.
if node.Name == pathComponents[0] {
if node.Name == pathComponents[0] || pathComponents[0] == "/" {
switch {
case l == 1 && dump.IsFile(node):
return dump.GetNodeData(ctx, os.Stdout, repo, node)
case l > 1 && dump.IsDir(node):
case l == 1 && node.Type == "file":
return getNodeData(ctx, os.Stdout, repo, node)
case l > 1 && node.Type == "dir":
subtree, err := repo.LoadTree(ctx, *node.Subtree)
if err != nil {
return errors.Wrapf(err, "cannot load subtree for %q", item)
}
return printFromTree(ctx, subtree, repo, item, pathComponents[1:])
case dump.IsDir(node):
if err := checkStdoutTar(); err != nil {
return err
}
subtree, err := repo.LoadTree(ctx, *node.Subtree)
if err != nil {
return err
}
return dump.WriteTar(ctx, repo, subtree, item, os.Stdout)
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], pathToPrint)
case node.Type == "dir":
node.Path = pathToPrint
return tarTree(ctx, repo, node, pathToPrint)
case l > 1:
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)
}
}
@@ -176,7 +162,7 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
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 {
Exitf(2, "cannot dump file: %v", err)
}
@@ -184,9 +170,126 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
return nil
}
func checkStdoutTar() error {
if stdoutIsTerminal() {
return fmt.Errorf("stdout is the terminal, please redirect output")
func getNodeData(ctx context.Context, output io.Writer, repo restic.Repository, node *restic.Node) error {
var (
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
}
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
}

View File

@@ -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)
}
}

View File

@@ -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())
return false, walker.ErrSkipNode
return false, walker.SkipNode
}
if node == nil {
@@ -314,7 +314,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
if !childMayMatch {
ignoreIfNoMatch = true
errIfNoMatch = walker.ErrSkipNode
errIfNoMatch = walker.SkipNode
} else {
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())
return false, walker.ErrSkipNode
return false, walker.SkipNode
}
if node == nil {
@@ -417,7 +417,7 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
packsFound := 0
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 {
return nil
}
@@ -465,8 +465,8 @@ func (f *Finder) findObjectPack(ctx context.Context, id string, t restic.BlobTyp
return
}
blobs := idx.Lookup(rid, t)
if len(blobs) == 0 {
blobs, found := idx.Lookup(rid, t)
if !found {
Printf("Object %s not found in the index\n", rid.Str())
return
}

View File

@@ -94,22 +94,34 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
return err
}
removeSnapshots := 0
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
var snapshots restic.Snapshots
removeSnIDs := restic.NewIDSet()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
snapshots = append(snapshots, sn)
}
var jsonGroups []*ForgetGroup
if len(args) > 0 {
// When explicit snapshots args are given, remove them immediately.
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 {
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)
}
var jsonGroups []*ForgetGroup
for k, snapshotGroup := range snapshotGroups {
if gopts.Verbose >= 1 && !gopts.JSON {
err = PrintSnapshotGroupHeader(gopts.stdout, k)
@@ -177,37 +191,37 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
jsonGroups = append(jsonGroups, &fg)
for _, sn := range remove {
removeSnIDs.Insert(*sn.ID())
removeSnapshots += len(remove)
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 {
err := DeleteFilesChecked(gopts, repo, removeSnIDs, restic.SnapshotFile)
if err != nil {
return err
}
} else {
if !gopts.JSON {
Printf("Would have removed the following snapshots:\n%v\n\n", removeSnIDs)
}
return pruneRepository(gopts, repo)
}
}
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
}

View File

@@ -9,7 +9,7 @@ import (
)
var cmdGenerate = &cobra.Command{
Use: "generate [flags]",
Use: "generate [command]",
Short: "Generate manual pages and auto-completion files (bash, zsh)",
Long: `
The "generate" command writes automatically generated files (like the man pages

View File

@@ -1,7 +1,6 @@
package main
import (
"github.com/restic/chunker"
"github.com/restic/restic/internal/errors"
"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,
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() {
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 == "" {
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)
if err != nil {
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)
err = s.Init(gopts.ctx, gopts.password, chunkerPolynomial)
err = s.Init(gopts.ctx, gopts.password)
if err != nil {
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
}
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
}

View File

@@ -16,7 +16,7 @@ import (
)
var cmdKey = &cobra.Command{
Use: "key [flags] [list|add|remove|passwd] [ID]",
Use: "key [list|add|remove|passwd] [ID]",
Short: "Manage keys (passwords)",
Long: `
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 (
newPasswordFile string
keyUsername string
keyHostname string
)
var newPasswordFile string
func init() {
cmdRoot.AddCommand(cmdKey)
flags := cmdKey.Flags()
flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "`file` from which to read the new password")
flags.StringVarP(&keyUsername, "user", "", "", "the username for new keys")
flags.StringVarP(&keyHostname, "host", "", "", "the hostname for new keys")
flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "the file from which to load a new password")
}
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error {
@@ -116,7 +110,7 @@ func getNewPassword(gopts GlobalOptions) (string, error) {
newopts.password = ""
return ReadPasswordTwice(newopts,
"enter new password: ",
"enter password for new key: ",
"enter password again: ")
}
@@ -126,7 +120,7 @@ func addKey(gopts GlobalOptions, repo *repository.Repository) error {
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 {
return errors.Fatalf("creating new key failed: %v\n", err)
}
@@ -157,7 +151,7 @@ func changePassword(gopts GlobalOptions, repo *repository.Repository) error {
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 {
return errors.Fatalf("creating new key failed: %v\n", err)
}

View File

@@ -1,15 +1,17 @@
package main
import (
"fmt"
"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/spf13/cobra"
)
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",
Long: `
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
switch args[0] {
case "packs":
t = restic.PackFile
t = restic.DataFile
case "index":
t = restic.IndexFile
case "snapshots":
@@ -60,17 +62,18 @@ func runList(cmd *cobra.Command, opts GlobalOptions, args []string) error {
case "locks":
t = restic.LockFile
case "blobs":
return repo.List(opts.ctx, restic.IndexFile, func(id restic.ID, size int64) error {
idx, err := repository.LoadIndex(opts.ctx, repo, id)
if err != nil {
return err
}
for blobs := range idx.Each(opts.ctx) {
Printf("%v %v\n", blobs.Type, blobs.ID)
}
return nil
})
idx, err := index.Load(opts.ctx, repo, nil)
if err != nil {
return err
}
for _, pack := range idx.Packs {
for _, entry := range pack.Entries {
fmt.Printf("%v %v\n", entry.Type, entry.ID)
}
}
return nil
default:
return errors.Fatal("invalid type")
}

View File

@@ -16,7 +16,7 @@ import (
)
var cmdLs = &cobra.Command{
Use: "ls [flags] snapshotID [dir...]",
Use: "ls [flags] [snapshotID] [dir...]",
Short: "List files in a snapshot",
Long: `
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 {
if len(args) == 0 {
return errors.Fatal("no snapshot ID specified")
if len(args) == 0 && len(opts.Hosts) == 0 && len(opts.Tags) == 0 && len(opts.Paths) == 0 {
return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
}
// 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
// subdirs
if node.Type == "dir" {
return false, walker.ErrSkipNode
return false, walker.SkipNode
}
return false, nil
})

View File

@@ -8,7 +8,7 @@ import (
)
var cmdMigrate = &cobra.Command{
Use: "migrate [flags] [name]",
Use: "migrate [name]",
Short: "Apply migrations",
Long: `
The "migrate" command applies migrations to a repository. When no migration

View File

@@ -1,4 +1,7 @@
// +build darwin freebsd linux
// +build !netbsd
// +build !openbsd
// +build !solaris
// +build !windows
package main
@@ -90,12 +93,10 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
return err
}
if !gopts.NoLock {
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
err = repo.LoadIndex(gopts.ctx)
@@ -141,7 +142,10 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
Paths: opts.Paths,
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("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)
err := umount(mountpoint)
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
})

View File

@@ -1,6 +1,9 @@
package main
import (
"fmt"
"time"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/index"
@@ -44,6 +47,34 @@ func shortenStatus(maxLength int, s string) string {
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 {
repo, err := OpenRepository(gopts)
if err != nil {
@@ -56,9 +87,6 @@ func runPrune(gopts GlobalOptions) error {
return err
}
// we do not need index updates while pruning!
repo.DisableAutoIndexUpdate()
return pruneRepository(gopts, repo)
}
@@ -97,7 +125,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
}
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++
return nil
})
@@ -155,22 +183,34 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
stats.snapshots = len(snapshots)
usedBlobs, err := getUsedBlobs(gopts, repo, snapshots)
if err != nil {
return err
}
Verbosef("find data that is still in use for %d snapshots\n", stats.snapshots)
var missingBlobs []restic.BlobHandle
for h := range usedBlobs {
if _, ok := blobCount[h]; !ok {
missingBlobs = append(missingBlobs, h)
usedBlobs := restic.NewBlobSet()
seenBlobs := restic.NewBlobSet()
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 {
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"+
"Please report this error (along with the output of the 'prune' run) at\n"+
"https://github.com/restic/restic/issues/new/choose", missingBlobs)
bar.Done()
if len(usedBlobs) > stats.blobs {
return errors.Fatalf("number of used blobs is larger than number of available blobs!\n" +
"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",
@@ -238,11 +278,13 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
var obsoletePacks restic.IDSet
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)
if err != nil {
return err
}
bar.Done()
}
removePacks.Merge(obsoletePacks)
@@ -252,38 +294,19 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
}
if len(removePacks) != 0 {
Verbosef("remove %d old packs\n", len(removePacks))
DeleteFiles(gopts, repo, removePacks, restic.PackFile)
bar = newProgressMax(!gopts.Quiet, uint64(len(removePacks)), "packs deleted")
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")
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
}

View File

@@ -53,7 +53,7 @@ func rebuildIndex(ctx context.Context, repo restic.Repository, ignorePacks resti
Verbosef("counting files in repo\n")
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++
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("remove %d old index files\n", len(supersedes))
err = DeleteFilesChecked(globalOptions, repo, restic.NewIDSet(supersedes...), restic.IndexFile)
if err != nil {
return errors.Fatalf("unable to remove an old index: %v\n", err)
for _, id := range supersedes {
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

View File

@@ -130,6 +130,11 @@ func runRecover(gopts GlobalOptions) error {
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())
if err != nil {
return errors.Fatalf("unable to save snapshot: %v", err)

View File

@@ -14,7 +14,7 @@ import (
)
var cmdSnapshots = &cobra.Command{
Use: "snapshots [flags] [snapshotID ...]",
Use: "snapshots [snapshotID ...]",
Short: "List all snapshots",
Long: `
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.
func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
var key restic.SnapshotGroupKey
var err error
err := json.Unmarshal([]byte(groupKeyJSON), &key)
err = json.Unmarshal([]byte(groupKeyJSON), &key)
if err != nil {
return err
}

View File

@@ -2,31 +2,31 @@ package main
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/walker"
"github.com/minio/sha256-simd"
"github.com/spf13/cobra"
)
var cmdStats = &cobra.Command{
Use: "stats [flags] [snapshot ID] [...]",
Use: "stats [flags] [snapshot-ID]",
Short: "Scan the repository and show basic statistics",
Long: `
The "stats" command walks one or multiple snapshots in a repository
and accumulates statistics about the data stored therein. It reports
on the number of unique files and their sizes, according to one of
The "stats" command walks one or all snapshots in a repository and
accumulates statistics about the data stored therein. It reports on
the number of unique files and their sizes, according to one of
the counting modes as given by the --mode flag.
It operates on all snapshots matching the selection criteria or all
snapshots if nothing is specified. The special snapshot ID "latest"
is also supported. Some modes make more sense over
just a single snapshot, while others are useful across all snapshots,
depending on what you are trying to calculate.
If no snapshot is specified, all snapshots will be considered. Some
modes make more sense over just a single snapshot, while others
are useful across all snapshots, depending on what you are trying
to calculate.
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() {
cmdRoot.AddCommand(cmdStats)
f := cmdStats.Flags()
f.StringVar(&statsOptions.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.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)")
f.StringVar(&countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file, or raw-data")
f.StringArrayVarP(&snapshotByHosts, "host", "H", nil, "filter latest snapshot by this hostname (can be specified multiple times)")
}
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)
stats := &statsContainer{
uniqueFiles: make(map[fileID]struct{}),
uniqueInodes: make(map[uint64]struct{}),
fileBlobs: make(map[string]restic.IDSet),
blobs: restic.NewBlobSet(),
snapshotsCount: 0,
uniqueFiles: make(map[fileID]struct{}),
uniqueInodes: make(map[uint64]struct{}),
fileBlobs: make(map[string]restic.IDSet),
blobs: restic.NewBlobSet(),
blobsSeen: restic.NewBlobSet(),
}
for sn := range FindFilteredSnapshots(ctx, repo, statsOptions.Hosts, statsOptions.Tags, statsOptions.Paths, args) {
err = statsWalkSnapshot(ctx, sn, repo, stats)
if snapshotIDString != "" {
// 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 {
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 {
return err
}
if statsOptions.countMode == countModeRawData {
if countMode == countModeRawData {
// the blob handles have been collected, but not yet counted
for blobHandle := range stats.blobs {
blobSize, found := repo.LookupBlobSize(blobHandle.ID, blobHandle.Type)
@@ -135,23 +148,29 @@ func runStats(gopts GlobalOptions, args []string) error {
}
if gopts.JSON {
err = json.NewEncoder(globalOptions.stdout).Encode(stats)
err = json.NewEncoder(os.Stdout).Encode(stats)
if err != nil {
return fmt.Errorf("encoding output: %v", err)
}
return nil
}
Printf("Stats in %s mode:\n", statsOptions.countMode)
Printf("Snapshots processed: %d\n", stats.snapshotsCount)
// inform the user what was scanned and how it was scanned
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 {
Printf(" Total Blob Count: %d\n", stats.TotalBlobCount)
Printf(" Total Blob Count: %d\n", stats.TotalBlobCount)
}
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
}
@@ -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())
}
stats.snapshotsCount++
if statsOptions.countMode == countModeRawData {
if countMode == countModeRawData {
// count just the sizes of unique blobs; we don't need to walk the tree
// ourselves in this case, since a nifty function does it for us
return 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))
if err != nil {
return fmt.Errorf("walking tree %s: %v", *snapshot.Tree, err)
}
return nil
}
@@ -186,19 +202,19 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun
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
fid := makeFileIDByContents(node)
if _, ok := stats.uniqueFiles[fid]; !ok {
// mark the file as visited
stats.uniqueFiles[fid] = struct{}{}
if statsOptions.countMode == countModeUniqueFilesByContents {
if countMode == countModeUniqueFilesByContents {
// simply count the size of each unique file (unique by contents only)
stats.TotalSize += node.Size
stats.TotalFileCount++
}
if statsOptions.countMode == countModeBlobsPerFile {
if countMode == countModeBlobsPerFile {
// count the size of each unique blob reference, which is
// by unique file (unique by contents and file path)
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
// size without worrying about uniqueness, since duplicate files
// will still be restored
@@ -260,13 +276,23 @@ func makeFileIDByContents(node *restic.Node) fileID {
func verifyStatsInput(gopts GlobalOptions, args []string) error {
// require a recognized counting mode
switch statsOptions.countMode {
switch countMode {
case countModeRestoreSize:
case countModeUniqueFilesByContents:
case countModeBlobsPerFile:
case countModeRawData:
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
@@ -292,17 +318,26 @@ type statsContainer struct {
// blobs that have been seen as a part of the file
fileBlobs map[string]restic.IDSet
// blobs is used to count individual unique blobs,
// independent of references to files
blobs restic.BlobSet
// holds count of all considered snapshots
snapshotsCount int
// blobs and blobsSeen are used to count individual
// unique blobs, independent of references to files
blobs, blobsSeen restic.BlobSet
}
// fileID is a 256-bit hash that distinguishes unique files.
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 (
countModeRestoreSize = "restore-size"
countModeUniqueFilesByContents = "files-by-contents"

View File

@@ -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
}

View File

@@ -6,7 +6,6 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
@@ -132,7 +131,7 @@ func rejectIfPresent(excludeFileSpec string) (RejectByNameFunc, error) {
}
// 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
// header. If rc is non-nil, it is used to expedite the evaluation of a
// 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)
return false
}
if !bytes.Equal(buf, []byte(header)) {
if bytes.Compare(buf, []byte(header)) != 0 {
Warnf("invalid signature in exclusion tagfile %q\n", tf)
return false
}
@@ -293,50 +292,3 @@ func rejectResticCache(repo *repository.Repository) (RejectByNameFunc, error) {
return false
}, 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
}

View File

@@ -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])
}
}
}

View File

@@ -22,10 +22,10 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
// Process all snapshot IDs given as arguments.
for _, s := range snapshotIDs {
if s == "latest" {
usedFilter = true
id, err = restic.FindLatestSnapshot(ctx, repo, paths, tags, hosts)
if err != nil {
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Hosts:%v)\n", s, paths, tags, hosts)
usedFilter = true
continue
}
} else {

View File

@@ -51,6 +51,12 @@ func formatPercent(numerator uint64, denominator uint64) string {
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 {
sec := uint64(d / time.Second)
return formatSeconds(sec)

View File

@@ -39,13 +39,11 @@ import (
"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.
const TimeFormat = "2006-01-02 15:04:05"
type backendWrapper func(r restic.Backend) (restic.Backend, error)
// GlobalOptions hold all global options for restic.
type GlobalOptions struct {
Repo string
@@ -70,13 +68,11 @@ type GlobalOptions struct {
stdout io.Writer
stderr io.Writer
backendTestHook backendWrapper
// verbosity is set as follows:
// 0 means: don't print any messages except errors, this is used when --quiet is specified
// 1 is the default: print essential messages
// 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
Options []string
@@ -101,11 +97,11 @@ func init() {
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.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.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.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.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)")
@@ -274,7 +270,7 @@ func Exitf(exitcode int, format string, args ...interface{}) {
}
// 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 != "" {
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")
}
if pwd := os.Getenv(envStr); pwd != "" {
if pwd := os.Getenv("RESTIC_PASSWORD"); pwd != "" {
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)
})
// 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)
passwordTriesLeft := 1

View File

@@ -1,7 +1,6 @@
package main
import (
"bytes"
"context"
"fmt"
"io/ioutil"
@@ -55,7 +54,7 @@ func walkDir(dir string) <-chan *dirEntry {
}()
// first element is root
<-ch
_ = <-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
// contain exactly the same contents, then the diff is an empty string.
func directoriesContentsDiff(dir1, dir2 string) string {
var out bytes.Buffer
// directoriesEqualContents checks if both directories contain exactly the same
// contents.
func directoriesEqualContents(dir1, dir2 string) bool {
ch1 := walkDir(dir1)
ch2 := walkDir(dir2)
changes := false
var a, b *dirEntry
for {
var ok bool
@@ -106,27 +116,36 @@ func directoriesContentsDiff(dir1, dir2 string) string {
}
if ch1 == nil {
fmt.Fprintf(&out, "+%v\n", b.path)
fmt.Printf("+%v\n", b.path)
changes = true
} else if ch2 == nil {
fmt.Fprintf(&out, "-%v\n", a.path)
} else if !a.equals(&out, b) {
fmt.Printf("-%v\n", a.path)
changes = true
} else if !a.equals(b) {
if a.path < b.path {
fmt.Fprintf(&out, "-%v\n", a.path)
fmt.Printf("-%v\n", a.path)
changes = true
a = nil
continue
} else if a.path > b.path {
fmt.Fprintf(&out, "+%v\n", b.path)
fmt.Printf("+%v\n", b.path)
changes = true
b = nil
continue
} else {
fmt.Fprintf(&out, "%%%v\n", a.path)
fmt.Printf("%%%v\n", a.path)
changes = true
}
}
a, b = nil, nil
}
return out.String()
if changes {
return false
}
return true
}
type dirStat struct {

View File

@@ -4,26 +4,25 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"syscall"
)
func (e *dirEntry) equals(out io.Writer, other *dirEntry) bool {
func (e *dirEntry) equals(other *dirEntry) bool {
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
}
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
}
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
}
@@ -31,17 +30,17 @@ func (e *dirEntry) equals(out io.Writer, other *dirEntry) bool {
stat2, _ := other.fi.Sys().(*syscall.Stat_t)
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
}
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
}
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
}

View File

@@ -4,24 +4,23 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"os"
)
func (e *dirEntry) equals(out io.Writer, other *dirEntry) bool {
func (e *dirEntry) equals(other *dirEntry) bool {
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
}
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
}
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
}

View File

@@ -13,7 +13,6 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"syscall"
"testing"
@@ -51,11 +50,11 @@ func testRunInit(t testing.TB, opts GlobalOptions) {
restic.TestDisableCheckPolynomial(t)
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)
}
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)
defer cancel()
@@ -70,7 +69,7 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts
defer cleanup()
}
backupErr := runBackup(opts, gopts, term, target)
rtest.OK(t, runBackup(opts, gopts, term, target))
cancel()
@@ -78,13 +77,6 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts
if err != nil {
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 {
@@ -151,21 +143,6 @@ func testRunCheckOutput(gopts GlobalOptions) (string, error) {
}
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
}
@@ -192,7 +169,7 @@ func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string {
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 {
@@ -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))
rtest.Assert(t, len(forgets[0].Remove) == 2,
"Expected 2 snapshots to be removed, got %v", len(forgets[0].Remove))
return
}
func testRunPrune(t testing.TB, gopts GlobalOptions) {
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) {
env, cleanup := withTestEnvironment(t)
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{}
// first backup
@@ -327,9 +309,9 @@ func TestBackup(t *testing.T) {
for i, snapshotID := range snapshotIDs {
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
testRunRestore(t, env.gopts, restoredir, snapshotID)
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
testRunRestore(t, env.gopts, restoredir, snapshotIDs[0])
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")),
"directories are not equal")
}
testRunCheck(t, env.gopts)
@@ -339,7 +321,18 @@ func TestBackupNonExistingFile(t *testing.T) {
env, cleanup := withTestEnvironment(t)
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
defer func() {
globalOptions.stderr = os.Stderr
@@ -358,58 +351,6 @@ func TestBackupNonExistingFile(t *testing.T) {
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 {
for _, s := range haystack {
if s == needle {
@@ -464,7 +405,7 @@ func TestBackupExclude(t *testing.T) {
f, err := os.Create(fp)
rtest.OK(t, err)
fmt.Fprint(f, filename)
fmt.Fprintf(f, filename)
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")
}
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 (
incrementalFirstWrite = 10 * 1042 * 1024
incrementalSecondWrite = 1 * 1042 * 1024
@@ -591,7 +506,10 @@ func TestBackupTags(t *testing.T) {
env, cleanup := withTestEnvironment(t)
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{}
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)
}
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) {
rtest.OK(t, runTag(opts, gopts, []string{}))
}
@@ -769,7 +540,10 @@ func TestTag(t *testing.T) {
env, cleanup := withTestEnvironment(t)
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)
testRunCheck(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"}))
}
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) {
testKeyNewPassword = newPassword
defer func() {
@@ -929,8 +681,6 @@ func TestKeyAddRemove(t *testing.T) {
t.Logf("testing access with last password %q\n", env.gopts.password)
rtest.OK(t, runKey(env.gopts, []string{"list"}))
testRunCheck(t, env.gopts)
testRunKeyAddNewKeyUserHost(t, env.gopts)
}
func testFileSize(filename string, size int64) error {
@@ -1017,8 +767,8 @@ func TestRestore(t *testing.T) {
restoredir := filepath.Join(env.base, "restore")
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata)))
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata))),
"directories are not equal")
}
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"})
f1 := filepath.Join(env.base, "restore0", "testdata", "subdir1", "subdir2")
_, err := os.Stat(f1)
fi, err := os.Stat(f1)
rtest.OK(t, err)
// restore with filter "*", this should restore meta data on everything.
testRunRestoreIncludes(t, env.gopts, filepath.Join(env.base, "restore1"), snapshotID, []string{"*"})
f2 := filepath.Join(env.base, "restore1", "testdata", "subdir1", "subdir2")
fi, err := os.Stat(f2)
fi, err = os.Stat(f2)
rtest.OK(t, err)
rtest.Assert(t, fi.ModTime() == time.Unix(0, 0),
@@ -1169,7 +919,10 @@ func TestFind(t *testing.T) {
env, cleanup := withTestEnvironment(t)
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{}
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
@@ -1206,7 +959,10 @@ func TestFindJSON(t *testing.T) {
env, cleanup := withTestEnvironment(t)
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{}
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
@@ -1267,37 +1023,6 @@ func TestRebuildIndexAlwaysFull(t *testing.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) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
@@ -1329,7 +1054,18 @@ func TestPrune(t *testing.T) {
env, cleanup := withTestEnvironment(t)
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{}
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)
}
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) {
// this test assumes a test set with a single directory containing hard linked files
env, cleanup := withTestEnvironment(t)
@@ -1436,9 +1120,9 @@ func TestHardLink(t *testing.T) {
for i, snapshotID := range snapshotIDs {
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
testRunRestore(t, env.gopts, restoredir, snapshotID)
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
testRunRestore(t, env.gopts, restoredir, snapshotIDs[0])
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")),
"directories are not equal")
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
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 {
@@ -1500,7 +1188,18 @@ func TestQuietBackup(t *testing.T) {
env, cleanup := withTestEnvironment(t)
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{}
env.gopts.Quiet = false
@@ -1519,91 +1218,3 @@ func TestQuietBackup(t *testing.T) {
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)
}
}

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