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
316 changed files with 5476 additions and 10754 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

@@ -16,8 +16,7 @@ help also.
The restic project uses the GitHub infrastructure (see the
[project page](https://github.com/restic/restic)) for all related discussions
as well as the [forum](https://forum.restic.net/) and the `#restic` channel
on [irc.freenode.net](https://kiwiirc.com/nextclient/irc.freenode.net/restic).
as well as the `#restic` channel on `irc.freenode.net`.
If you want to find an area that currently needs improving have a look at the
open issues listed at the
@@ -26,10 +25,7 @@ for discussing enhancement to the restic tools.
If you are unsure what to do, please have a look at the issues, especially
those tagged
[minor complexity](https://github.com/restic/restic/labels/help%3A%20minor%20complexity)
or [good first issue](https://github.com/restic/restic/labels/help%3A%20good%20first%20issue).
If you are already a bit experienced with the restic internals, take a look
at the issues tagged as [help wanted](https://github.com/restic/restic/labels/help%3A%20wanted).
[minor complexity](https://github.com/restic/restic/labels/minor%20complexity).
Reporting Bugs
@@ -64,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.13 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
@@ -78,12 +79,40 @@ Then use the `go` tool to build restic:
$ go build ./cmd/restic
$ ./restic version
restic 0.10.0-dev (compiled manually) compiled with go1.15.2 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
=================
@@ -96,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.
@@ -116,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,4 +1,4 @@
|Documentation| |Build Status| |Build status| |Report Card| |Reviewed by Hound|
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |Reviewed by Hound|
Introduction
------------
@@ -127,6 +127,8 @@ Storage are sponsored by `AppsCode <https://appscode.com>`__!
:target: https://ci.appveyor.com/project/fd0/restic/branch/master
.. |Report Card| image:: https://goreportcard.com/badge/github.com/restic/restic
:target: https://goreportcard.com/report/github.com/restic/restic
.. |Say Thanks| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
:target: https://saythanks.io/to/restic
.. |AppsCode| image:: https://cdn.appscode.com/images/logo/appscode/ac-logo-color.png
:target: https://appscode.com
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg

View File

@@ -1 +1 @@
0.10.0
0.9.6

View File

@@ -20,11 +20,11 @@ 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 https://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip
- 7z x tar.zip bin/tar.exe
- set PATH=bin/;%PATH%

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

@@ -1,21 +1,11 @@
# The first line must start with Bugfix:, Enhancement: or Change:,
# including the colon. Use present use. Remove lines starting with '#'
# from this template.
Enhancement: Allow custom bar in the foo command
Bugfix: Fix behavior for foobar (in present tense)
# Describe the problem in the past tense, the new behavior in the present
# tense. Mention the affected commands, backends, operating systems, etc.
# Focus on user-facing behavior, not the implementation.
We've fixed the behavior for foobar, a long-standing annoyance for restic
users.
Restic foo always used the system-wide bar when deciding how to frob an
item in the baz backend. It now permits selecting the bar with --bar or
the environment variable RESTIC_BAR. The system-wide bar is still the
default.
# The last section is a list of issue, PR and forum URLs.
# The first issue ID determines the filename for the changelog entry:
# changelog/unreleased/issue-1234. If there are no relevant issue links,
# use the PR ID and call the file pull-55555.
The text in the paragraphs is written in past tense. The last section is a list
of issue URLs, PR URLs and other URLs. The first issue ID (or the first PR ID,
in case there aren't any issue links) is used as the primary ID.
https://github.com/restic/restic/issues/1234
https://github.com/restic/restic/pull/55555

View File

@@ -1,11 +0,0 @@
Bugfix: Restore timestamps and permissions on intermediate directories
When using the `--include` option of the restore command, restic restored
timestamps and permissions only on directories selected by the include pattern.
Intermediate directories, which are necessary to restore files located in sub-
directories, were created with default permissions. We've fixed the restore
command to restore timestamps and permissions for these directories as well.
https://github.com/restic/restic/issues/1212
https://github.com/restic/restic/issues/1402
https://github.com/restic/restic/pull/2906

View File

@@ -1,15 +0,0 @@
Bugfix: Mark repository files as read-only when using the local backend
Files stored in a local repository were marked as writeable on the
filesystem for non-Windows systems, which did not prevent accidental file
modifications outside of restic. In addition, the local backend did not work
with certain filesystems and network mounts which do not permit modifications
of file permissions.
restic now marks files stored in a local repository as read-only on the
filesystem on non-Windows systems. The error handling is improved to support
more filesystems.
https://github.com/restic/restic/issues/1756
https://github.com/restic/restic/issues/2157
https://github.com/restic/restic/pull/2989

View File

@@ -1,10 +0,0 @@
Bugfix: Hide password in REST backend repository URLs
When using a password in the REST backend repository URL,
the password could in some cases be included in the output
from restic, e.g. when initializing a repo or during an error.
The password is now replaced with "***" where applicable.
https://github.com/restic/restic/issues/2241
https://github.com/restic/restic/pull/2658

View File

@@ -1,9 +0,0 @@
Bugfix: Don't require `self-update --output` placeholder file
`restic self-update --output /path/to/new-restic` used to require that
new-restic was an existing file, to be overwritten. Now it's possible
to download an updated restic binary to a new path, without first
having to create a placeholder file.
https://github.com/restic/restic/issues/2491
https://github.com/restic/restic/pull/2937

View File

@@ -1,7 +0,0 @@
Bugfix: Fix rare cases of backup command hanging forever
We've fixed an issue with the backup progress reporting which could cause
restic to hang forever right before finishing a backup.
https://github.com/restic/restic/issues/2834
https://github.com/restic/restic/pull/2963

View File

@@ -1,6 +0,0 @@
Bugfix: Fix manpage formatting
The manpage formatting in restic v0.10.0 was garbled, which is fixed now.
https://github.com/restic/restic/issues/2938
https://github.com/restic/restic/pull/2977

View File

@@ -1,7 +0,0 @@
Bugfix: Make --exclude-larger-than handle disappearing files
There was a small bug in the backup command's --exclude-larger-than
option where files that disappeared between scanning and actually
backing them up to the repository caused a panic. This is now fixed.
https://github.com/restic/restic/issues/2942

View File

@@ -1,9 +0,0 @@
Bugfix: restic generate, help and self-update no longer check passwords
The commands `restic cache`, `generate`, `help` and `self-update` don't need
passwords, but they previously did run the RESTIC_PASSWORD_COMMAND (if set in
the environment), prompting users to authenticate for no reason. They now skip
running the password command.
https://github.com/restic/restic/issues/2951
https://github.com/restic/restic/pull/2987

View File

@@ -1,9 +0,0 @@
Enhancement: Optimize check for unchanged files during backup
During a backup restic skips processing files which have not changed since the last backup run.
Previously this required opening each file once which can be slow on network filesystems. The
backup command now checks for file changes before opening a file. This considerably reduces
the time to create a backup on network filesystems.
https://github.com/restic/restic/issues/2969
https://github.com/restic/restic/pull/2970

View File

@@ -1,9 +0,0 @@
Bugfix: Make snapshots --json output [] instead of null when no snapshots
Restic previously output `null` instead of `[]` for the `--json snapshots`
command, when there were no snapshots in the repository. This caused some
minor problems when parsing the output, but is now fixed such that `[]` is
output when the list of snapshots is empty.
https://github.com/restic/restic/issues/2979
https://github.com/restic/restic/pull/2984

View File

@@ -1,12 +0,0 @@
Enhancement: Add support for Volume Shadow Copy Service (VSS) on Windows
Volume Shadow Copy Service allows read access to files that are locked by
another process using an exclusive lock through a filesystem snapshot. Restic
was unable to backup those files before. This update enables backing up these
files.
This needs to be enabled explicitely using the --use-fs-snapshot option of the
backup command.
https://github.com/restic/restic/issues/340
https://github.com/restic/restic/pull/2274

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

View File

@@ -1,7 +0,0 @@
Enhancement: Authenticate to Google Cloud Storage with access token
When using the GCS backend, it is now possible to authenticate with OAuth2
access tokens instead of a credentials file by setting the GOOGLE_ACCESS_TOKEN
environment variable.
https://github.com/restic/restic/pull/2849

View File

@@ -1,10 +0,0 @@
Enhancement: New option --repository-file
We've added a new command-line option --repository-file as an alternative
to -r. This allows to read the repository URL from a file in order to
prevent certain types of information leaks, especially for URLs containing
credentials.
https://github.com/restic/restic/issues/1458
https://github.com/restic/restic/issues/2900
https://github.com/restic/restic/pull/2910

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

@@ -10,7 +10,6 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
@@ -40,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 == "" {
@@ -79,32 +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
UseFsSnapshot 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)
@@ -114,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)")
@@ -131,9 +123,6 @@ func init() {
f.StringVar(&backupOptions.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories")
f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number changes when checking for modified files")
if runtime.GOOS == "windows" {
f.BoolVar(&backupOptions.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
}
}
// filterExisting returns a slice of all existing items, or an error if no
@@ -248,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))
}
@@ -292,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
}
@@ -399,7 +372,7 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
func findParentSnapshot(ctx context.Context, repo restic.Repository, opts BackupOptions, targets []string) (parentID *restic.ID, err error) {
// Force using a parent
if !opts.Force && opts.Parent != "" {
id, err := restic.FindSnapshot(ctx, repo, opts.Parent)
id, err := restic.FindSnapshot(repo, opts.Parent)
if err != nil {
return nil, errors.Fatalf("invalid id %q: %v", opts.Parent, err)
}
@@ -442,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)
@@ -501,7 +474,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
if !gopts.JSON {
p.V("lock repository")
}
lock, err := lockRepo(gopts.ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
@@ -555,25 +528,6 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
}
var targetFS fs.FS = fs.Local{}
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
if !fs.HasSufficientPrivilegesForVSS() {
return errors.Fatal("user doesn't have sufficient privileges to use VSS snapshots\n")
}
errorHandler := func(item string, err error) error {
return p.Error(item, nil, err)
}
messageHandler := func(msg string, args ...interface{}) {
if !gopts.JSON {
p.P(msg, args...)
}
}
localVss := fs.NewLocalVss(errorHandler, messageHandler)
defer localVss.DeleteSnapshots()
targetFS = localVss
}
if opts.Stdin {
if !gopts.JSON {
p.V("read data from stdin")
@@ -603,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
@@ -625,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)
}
@@ -644,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

@@ -51,7 +51,7 @@ func init() {
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
if len(args) > 0 {
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
return errors.Fatal("the cache command has no arguments")
}
if gopts.NoCache {

View File

@@ -2,6 +2,8 @@ package main
import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
@@ -42,7 +44,7 @@ func runCat(gopts GlobalOptions, args []string) error {
return err
}
lock, err := lockRepo(gopts.ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
@@ -59,7 +61,7 @@ func runCat(gopts GlobalOptions, args []string) error {
}
// find snapshot id with prefix
id, err = restic.FindSnapshot(gopts.ctx, repo, args[1])
id, err = restic.FindSnapshot(repo, args[1])
if err != nil {
return errors.Fatalf("could not find snapshot: %v\n", err)
}
@@ -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
@@ -142,7 +174,7 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func())
func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 {
return errors.Fatal("the check command expects no arguments, only options - please see `restic help check` for usage and flags")
return errors.Fatal("check has no arguments")
}
cleanup := prepareCheckCache(opts, &gopts)
@@ -158,7 +190,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
if !gopts.NoLock {
Verbosef("create exclusive lock for repository\n")
lock, err := lockRepoExclusive(gopts.ctx, repo)
lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock)
if err != nil {
return err
@@ -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(ctx, srcRepo)
defer unlockRepo(srcLock)
if err != nil {
return err
}
dstLock, err := lockRepo(ctx, 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

@@ -54,9 +54,9 @@ func prettyPrintJSON(wr io.Writer, item interface{}) error {
return err
}
func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io.Writer) error {
return repo.List(ctx, restic.SnapshotFile, func(id restic.ID, size int64) error {
snapshot, err := restic.LoadSnapshot(ctx, repo, id)
func debugPrintSnapshots(repo *repository.Repository, wr io.Writer) error {
return repo.List(context.TODO(), restic.SnapshotFile, func(id restic.ID, size int64) error {
snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id)
if err != nil {
return err
}
@@ -82,14 +82,14 @@ type Blob struct {
Offset uint `json:"offset"`
}
func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer) error {
func printPacks(repo *repository.Repository, wr io.Writer) error {
return repo.List(ctx, 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(ctx, repo.Backend(), h), size)
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
}
@@ -110,11 +110,11 @@ func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer)
})
}
func dumpIndexes(ctx context.Context, repo restic.Repository, wr io.Writer) error {
return repo.List(ctx, restic.IndexFile, func(id restic.ID, size int64) error {
Printf("index_id: %v\n", id)
func dumpIndexes(repo restic.Repository, wr io.Writer) error {
return repo.List(context.TODO(), restic.IndexFile, func(id restic.ID, size int64) error {
fmt.Printf("index_id: %v\n", id)
idx, err := repository.LoadIndex(ctx, repo, id)
idx, err := repository.LoadIndex(context.TODO(), repo, id)
if err != nil {
return err
}
@@ -134,7 +134,7 @@ func runDebugDump(gopts GlobalOptions, args []string) error {
}
if !gopts.NoLock {
lock, err := lockRepo(gopts.ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
@@ -145,20 +145,20 @@ func runDebugDump(gopts GlobalOptions, args []string) error {
switch tpe {
case "indexes":
return dumpIndexes(gopts.ctx, repo, gopts.stdout)
return dumpIndexes(repo, gopts.stdout)
case "snapshots":
return debugPrintSnapshots(gopts.ctx, repo, gopts.stdout)
return debugPrintSnapshots(repo, gopts.stdout)
case "packs":
return printPacks(gopts.ctx, repo, gopts.stdout)
return printPacks(repo, gopts.stdout)
case "all":
Printf("snapshots:\n")
err := debugPrintSnapshots(gopts.ctx, repo, gopts.stdout)
fmt.Printf("snapshots:\n")
err := debugPrintSnapshots(repo, gopts.stdout)
if err != nil {
return err
}
Printf("\nindexes:\n")
err = dumpIndexes(gopts.ctx, repo, gopts.stdout)
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
@@ -53,7 +53,7 @@ func init() {
}
func loadSnapshot(ctx context.Context, repo *repository.Repository, desc string) (*restic.Snapshot, error) {
id, err := restic.FindSnapshot(ctx, repo, desc)
id, err := restic.FindSnapshot(repo, desc)
if err != nil {
return nil, err
}
@@ -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)
}
@@ -332,7 +305,7 @@ func runDiff(opts DiffOptions, gopts GlobalOptions, args []string) error {
}
if !gopts.NoLock {
lock, err := lockRepo(ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return 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)
}
}
@@ -140,7 +126,7 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
}
if !gopts.NoLock {
lock, err := lockRepo(ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
@@ -160,7 +146,7 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Hosts:%v", err, opts.Paths, opts.Hosts)
}
} else {
id, err = restic.FindSnapshot(ctx, repo, snapshotIDString)
id, err = restic.FindSnapshot(repo, snapshotIDString)
if err != nil {
Exitf(1, "invalid id %q: %v", snapshotIDString, err)
}
@@ -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
}
@@ -529,7 +529,7 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
}
if !gopts.NoLock {
lock, err := lockRepo(gopts.ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err

View File

@@ -73,7 +73,7 @@ func init() {
f.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")
f.StringArrayVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
f.BoolVarP(&forgetOptions.Compact, "compact", "c", false, "use compact output format")
f.BoolVarP(&forgetOptions.Compact, "compact", "c", false, "use compact format")
f.StringVarP(&forgetOptions.GroupBy, "group-by", "g", "host,paths", "string for grouping snapshots by host,paths,tags")
f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
@@ -88,28 +88,40 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
return err
}
lock, err := lockRepoExclusive(gopts.ctx, repo)
lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock)
if err != nil {
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,8 +1,6 @@
package main
import (
"github.com/restic/chunker"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository"
@@ -22,44 +20,22 @@ 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)
be, err := create(gopts.Repo, gopts.extended)
if err != nil {
return err
}
repo, err := ReadRepo(gopts)
if err != nil {
return err
}
be, err := create(repo, gopts.extended)
if err != nil {
return errors.Fatalf("create repository at %s failed: %v\n", location.StripPassword(gopts.Repo), err)
return errors.Fatalf("create repository at %s failed: %v\n", gopts.Repo, err)
}
gopts.password, err = ReadPasswordTwice(gopts,
@@ -71,12 +47,12 @@ 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", location.StripPassword(gopts.Repo), err)
return errors.Fatalf("create key in repository at %s failed: %v\n", gopts.Repo, err)
}
Verbosef("created restic repository %v at %s\n", s.Config().ID[:10], location.StripPassword(gopts.Repo))
Verbosef("created restic repository %v at %s\n", s.Config().ID[:10], gopts.Repo)
Verbosef("\n")
Verbosef("Please note that knowledge of your password is required to access\n")
Verbosef("the repository. Losing your password means that your data is\n")
@@ -84,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)
}
@@ -188,7 +182,7 @@ func runKey(gopts GlobalOptions, args []string) error {
switch args[0] {
case "list":
lock, err := lockRepo(ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
@@ -196,7 +190,7 @@ func runKey(gopts GlobalOptions, args []string) error {
return listKeys(ctx, repo, gopts)
case "add":
lock, err := lockRepo(ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
@@ -204,20 +198,20 @@ func runKey(gopts GlobalOptions, args []string) error {
return addKey(gopts, repo)
case "remove":
lock, err := lockRepoExclusive(ctx, repo)
lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
id, err := restic.Find(ctx, repo.Backend(), restic.KeyFile, args[1])
id, err := restic.Find(repo.Backend(), restic.KeyFile, args[1])
if err != nil {
return err
}
return deleteKey(gopts.ctx, repo, id)
case "passwd":
lock, err := lockRepoExclusive(ctx, repo)
lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock)
if err != nil {
return 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.
@@ -40,7 +42,7 @@ func runList(cmd *cobra.Command, opts GlobalOptions, args []string) error {
}
if !opts.NoLock {
lock, err := lockRepo(opts.ctx, repo)
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
@@ -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
@@ -99,7 +99,7 @@ func runMigrate(opts MigrateOptions, gopts GlobalOptions, args []string) error {
return err
}
lock, err := lockRepoExclusive(gopts.ctx, repo)
lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock)
if err != nil {
return err

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