Compare commits

..

16 Commits

Author SHA1 Message Date
Alexander Neumann
abb1dc4eb6 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
8d21bb92db Add tests for invalid configs 2018-06-10 12:27:52 +02:00
Alexander Neumann
0b3c402801 Move options package to ui/options 2018-06-10 12:27:52 +02:00
Alexander Neumann
b3b70002ab wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
4916ba7a8a wip name 2018-06-10 12:27:52 +02:00
Alexander Neumann
ea565df3e8 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
0758c92afc wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
8b0092908a wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
ffd7bc1021 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
6bad560324 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
7ad648c686 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
0c078cc205 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
1fbcf63830 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
740e2d6139 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
aaef54559a wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
722517c480 wip 2018-06-10 12:27:52 +02:00
7753 changed files with 5037802 additions and 202401 deletions

View File

@@ -1,9 +1,17 @@
<!--
Welcome! If you have a question or are unsure if you should open an issue,
please use the forum instead!
Welcome! - We kindly ask that you:
https://forum.restic.net
1. Fill out the issue template below - not doing so needs a good reason.
2. Use the forum if you have a question rather than a bug or feature request.
The forum is at: https://forum.restic.net
NOTE: Not filling out the issue template needs a good reason, as otherwise it
may take a lot longer to find the problem, not to mention it can take up a lot
more time which can otherwise be spent on development. Please also take the
time to help us debug the issue by collecting relevant information, even if
it doesn't seem to be relevant to you. Thanks!
The forum is a better place for questions about restic or general suggestions
and topics, e.g. usage or documentation questions! This issue tracker is mainly
@@ -11,17 +19,61 @@ for tracking bugs and feature requests directly relating to the development of
the software itself, rather than the project.
Thanks for understanding, and for contributing to the project!
-->
Output of `restic version`
--------------------------
## Output of `restic version`
## How did you run restic exactly?
<!--
Please add the version of restic you're currently using here, this helps us
later to see what has changed in restic when we revisit this issue after some
time.
This section should include at least:
* The complete command line and any environment variables you used to
configure restic's backend access. Make sure to replace sensitive values!
* The output of the commands, what restic prints gives may give us much
information to diagnose the problem!
-->
Describe the issue
------------------
## What backend/server/service did you use to store the repository?
## Expected behavior
<!--
Describe what you'd like restic to do differently.
-->
## Actual behavior
<!--
In this section, please try to concentrate on observations, so only describe
what you observed directly.
-->
## Steps to reproduce the behavior
<!--
The more time you spend describing an easy way to reproduce the behavior (if
this is possible), the easier it is for the project developers to fix it!
-->
## Do you have any idea what may have caused this?
## Do you have an idea how to solve the issue?
## Did restic help you or made you happy in any way?
<!--
Answering this question is not required, but if you have anything positive to share, please do so here!
Sometimes we get tired of reading bug reports all day and a little positive end note does wonders.
Idea by Joey Hess, https://joeyh.name/blog/entry/two_holiday_stories/
-->

View File

@@ -1,93 +0,0 @@
---
name: Bug report
about: Report a problem with restic to help us resolve it and improve
---
<!--
Welcome! - We kindly ask that you:
1. Fill out the issue template below - not doing so needs a good reason.
2. Use the forum if you have a question rather than a bug or feature request.
The forum is at: https://forum.restic.net
NOTE: Not filling out the issue template needs a good reason, as otherwise it
may take a lot longer to find the problem, not to mention it can take up a lot
more time which can otherwise be spent on development. Please also take the
time to help us debug the issue by collecting relevant information, even if
it doesn't seem to be relevant to you. Thanks!
The forum is a better place for questions about restic or general suggestions
and topics, e.g. usage or documentation questions! This issue tracker is mainly
for tracking bugs and feature requests directly relating to the development of
the software itself, rather than the project.
Thanks for understanding, and for contributing to the project!
-->
Output of `restic version`
--------------------------
How did you run restic exactly?
-------------------------------
<!--
This section should include at least:
* The complete command line and any environment variables you used to
configure restic's backend access. Make sure to replace sensitive values!
* The output of the commands, what restic prints gives may give us much
information to diagnose the problem!
-->
What backend/server/service did you use to store the repository?
----------------------------------------------------------------
Expected behavior
-----------------
<!--
Describe what you'd like restic to do differently.
-->
Actual behavior
---------------
<!--
In this section, please try to concentrate on observations, so only describe
what you observed directly.
-->
Steps to reproduce the behavior
-------------------------------
<!--
The more time you spend describing an easy way to reproduce the behavior (if
this is possible), the easier it is for the project developers to fix it!
-->
Do you have any idea what may have caused this?
-----------------------------------------------
Do you have an idea how to solve the issue?
-------------------------------------------
Did restic help you or made you happy in any way?
-------------------------------------------------
<!--
Answering this question is not required, but if you have anything positive to share, please do so here!
Sometimes we get tired of reading bug reports all day and a little positive end note does wonders.
Idea by Joey Hess, https://joeyh.name/blog/entry/two_holiday_stories/
-->

View File

@@ -1,57 +0,0 @@
---
name: Feature request
about: Suggest a new feature or enhancement for restic
---
<!--
Welcome! - We kindly ask that you:
1. Fill out the issue template below - not doing so needs a good reason.
2. Use the forum if you have a question rather than a bug or feature request.
The forum is at: https://forum.restic.net
The forum is a better place for questions about restic or general suggestions
and topics, e.g. usage or documentation questions! This issue tracker is mainly
for tracking bugs and feature requests directly relating to the development of
the software itself, rather than the project.
Thanks for understanding, and for contributing to the project!
-->
Output of `restic version`
--------------------------
<!--
Please add the version of restic you're currently using here, this helps us
later to see what has changed in restic when we revisit this issue after some
time.
-->
What should restic do differently? Which functionality do you think we should add?
----------------------------------------------------------------------------------
<!--
Please describe the feature you'd like us to add here.
-->
What are you trying to do?
--------------------------
<!--
This section should contain a brief description what you're trying to do, which
would be possible after implementing the new feature.
-->
Did restic help you or made you happy in any way?
-------------------------------------------------
<!--
Answering this question is not required, but if you have anything positive to share, please do so here!
Sometimes we get tired of reading bug reports all day and a little positive end note does wonders.
Idea by Joey Hess, https://joeyh.name/blog/entry/two_holiday_stories/
-->

View File

@@ -1,5 +1,3 @@
<!--
Thank you very much for contributing code or documentation to restic! Please
fill out the following questions to make it easier for us to review your
@@ -10,25 +8,19 @@ your time and add more commits. If you're done and ready for review, please
check the last box.
-->
What is the purpose of this change? What does it change?
--------------------------------------------------------
### What is the purpose of this change? What does it change?
<!--
Describe the changes here, as detailed as needed.
-->
Was the change discussed in an issue or in the forum before?
------------------------------------------------------------
### Was the change discussed in an issue or in the forum before?
<!--
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.
-->
Checklist
---------
### Checklist
- [ ] I have read the [Contribution Guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches)
- [ ] I have added tests for all changes in this PR

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/restic
/.vagrant
/doc/_build

View File

@@ -4,45 +4,17 @@ sudo: false
matrix:
include:
- os: linux
go: "1.10.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.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
go: "1.9.x"
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0 RESTIC_BUILD_SOLARIS=0
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
- os: linux
go: "1.13.x"
go: "1.10.x"
sudo: true
cache:
directories:
- $HOME/.cache/go-build
- $HOME/gopath/pkg/mod
- os: osx
go: "1.13.x"
go: "1.10.x"
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
cache:
directories:
- $HOME/Library/Caches/go-build
- $HOME/gopath/pkg/mod
branches:
only:
@@ -64,3 +36,6 @@ install:
script:
- go run run_integration_tests.go
after_success:
- bash <(curl -s https://codecov.io/bash) -f all.cov

View File

@@ -1,669 +1,3 @@
Changelog for restic 0.9.6 (2019-11-22)
=======================================
The following sections list the changes in restic 0.9.6 relevant to
restic users. The changes are ordered by importance.
Summary
-------
* Fix #2063: Allow absolute path for filename when backing up from stdin
* Fix #2174: Save files with invalid timestamps
* Fix #2249: Read fresh metadata for unmodified files
* Fix #2301: Add upper bound for t in --read-data-subset=n/t
* Fix #2321: Check errors when loading index files
* Enh #2306: Allow multiple retries for interactive password input
* Enh #2330: Make `--group-by` accept both singular and plural
* Enh #2350: Add option to configure S3 region
Details
-------
* Bugfix #2063: Allow absolute path for filename when backing up from stdin
When backing up from stdin, handle directory path for `--stdin-filename`. This can be used to
specify the full path for the backed-up file.
https://github.com/restic/restic/issues/2063
* Bugfix #2174: Save files with invalid timestamps
When restic reads invalid timestamps (year is before 0000 or after 9999) it refused to read and
archive the file. We've changed the behavior and will now save modified timestamps with the
year set to either 0000 or 9999, the rest of the timestamp stays the same, so the file will be saved
(albeit with a bogus timestamp).
https://github.com/restic/restic/issues/2174
https://github.com/restic/restic/issues/1173
* Bugfix #2249: Read fresh metadata for unmodified files
Restic took all metadata for files which were detected as unmodified, not taking into account
changed metadata (ownership, mode). This is now corrected.
https://github.com/restic/restic/issues/2249
https://github.com/restic/restic/pull/2252
* Bugfix #2301: Add upper bound for t in --read-data-subset=n/t
256 is the effective maximum for t, but restic would allow larger values, leading to strange
behavior.
https://github.com/restic/restic/issues/2301
https://github.com/restic/restic/pull/2304
* Bugfix #2321: Check errors when loading index files
Restic now checks and handles errors which occur when loading index files, the missing check
leads to odd errors (and a stack trace printed to users) later. This was reported in the forum.
https://github.com/restic/restic/pull/2321
https://forum.restic.net/t/check-rebuild-index-prune/1848/13
* Enhancement #2306: Allow multiple retries for interactive password input
Restic used to quit if the repository password was typed incorrectly once. Restic will now ask
the user again for the repository password if typed incorrectly. The user will now get three
tries to input the correct password before restic quits.
https://github.com/restic/restic/issues/2306
* Enhancement #2330: Make `--group-by` accept both singular and plural
One can now use the values `host`/`hosts`, `path`/`paths` and `tag` / `tags` interchangeably
in the `--group-by` argument.
https://github.com/restic/restic/issues/2330
* Enhancement #2350: Add option to configure S3 region
We've added a new option for setting the region when accessing an S3-compatible service. For
some providers, it is required to set this to a valid value. You can do that either by setting the
environment variable `AWS_DEFAULT_REGION` or using the option `s3.region`, e.g. like this:
`-o s3.region="us-east-1"`.
https://github.com/restic/restic/pull/2350
Changelog for restic 0.9.5 (2019-04-23)
=======================================
The following sections list the changes in restic 0.9.5 relevant to
restic users. The changes are ordered by importance.
Summary
-------
* Fix #2135: Return error when no bytes could be read from stdin
* Fix #2181: Don't cancel timeout after 30 seconds for self-update
* Fix #2203: Fix reading passwords from stdin
* Fix #2224: Don't abort the find command when a tree can't be loaded
* Enh #1895: Add case insensitive include & exclude options
* Enh #1937: Support streaming JSON output for backup
* Enh #2155: Add Openstack application credential auth for Swift
* Enh #2179: Use ctime when checking for file changes
* Enh #2184: Add --json support to forget command
* Enh #2037: Add group-by option to snapshots command
* Enh #2124: Ability to dump folders to tar via stdout
* Enh #2139: Return error if no bytes could be read for `backup --stdin`
* Enh #2205: Add --ignore-inode option to backup cmd
* Enh #2220: Add config option to set S3 storage class
Details
-------
* Bugfix #2135: Return error when no bytes could be read from stdin
We assume that users reading backup data from stdin want to know when no data could be read, so now
restic returns an error when `backup --stdin` is called but no bytes could be read. Usually,
this means that an earlier command in a pipe has failed. The documentation was amended and now
recommends setting the `pipefail` option (`set -o pipefail`).
https://github.com/restic/restic/pull/2135
https://github.com/restic/restic/pull/2139
* Bugfix #2181: Don't cancel timeout after 30 seconds for self-update
https://github.com/restic/restic/issues/2181
* Bugfix #2203: Fix reading passwords from stdin
Passwords for the `init`, `key add`, and `key passwd` commands can now be read from
non-terminal stdin.
https://github.com/restic/restic/issues/2203
* Bugfix #2224: Don't abort the find command when a tree can't be loaded
Change the find command so that missing trees don't result in a crash. Instead, the error is
logged to the debug log, and the tree ID is displayed along with the snapshot it belongs to. This
makes it possible to recover repositories that are missing trees by forgetting the snapshots
they are used in.
https://github.com/restic/restic/issues/2224
* Enhancement #1895: Add case insensitive include & exclude options
The backup and restore commands now have --iexclude and --iinclude flags as case insensitive
variants of --exclude and --include.
https://github.com/restic/restic/issues/1895
https://github.com/restic/restic/pull/2032
* Enhancement #1937: Support streaming JSON output for backup
We've added support for getting machine-readable status output during backup, just pass the
flag `--json` for `restic backup` and restic will output a stream of JSON objects which contain
the current progress.
https://github.com/restic/restic/issues/1937
https://github.com/restic/restic/pull/1944
* Enhancement #2155: Add Openstack application credential auth for Swift
Since Openstack Queens Identity (auth V3) service supports an application credential auth
method. It allows to create a technical account with the limited roles. This commit adds an
application credential authentication method for the Swift backend.
https://github.com/restic/restic/issues/2155
* Enhancement #2179: Use ctime when checking for file changes
Previously, restic only checked a file's mtime (along with other non-timestamp metadata) to
decide if a file has changed. This could cause restic to not notice that a file has changed (and
therefore continue to store the old version, as opposed to the modified version) if something
edits the file and then resets the timestamp. Restic now also checks the ctime of files, so any
modifications to a file should be noticed, and the modified file will be backed up. The ctime
check will be disabled if the --ignore-inode flag was given.
If this change causes problems for you, please open an issue, and we can look in to adding a
seperate flag to disable just the ctime check.
https://github.com/restic/restic/issues/2179
https://github.com/restic/restic/pull/2212
* Enhancement #2184: Add --json support to forget command
The forget command now supports the --json argument, outputting the information about what is
(or would-be) kept and removed from the repository.
https://github.com/restic/restic/issues/2184
https://github.com/restic/restic/pull/2185
* Enhancement #2037: Add group-by option to snapshots command
We have added an option to group the output of the snapshots command, similar to the output of the
forget command. The option has been called "--group-by" and accepts any combination of the
values "host", "paths" and "tags", separated by commas. Default behavior (not specifying
--group-by) has not been changed. We have added support of the grouping to the JSON output.
https://github.com/restic/restic/issues/2037
https://github.com/restic/restic/pull/2087
* Enhancement #2124: Ability to dump folders to tar via stdout
We've added the ability to dump whole folders to stdout via the `dump` command. Restic now
requires at least Go 1.10 due to a limitation of the standard library for Go <= 1.9.
https://github.com/restic/restic/issues/2123
https://github.com/restic/restic/pull/2124
* Enhancement #2139: Return error if no bytes could be read for `backup --stdin`
When restic is used to backup the output of a program, like `mysqldump | restic backup --stdin`,
it now returns an error if no bytes could be read at all. This catches the failure case when
`mysqldump` failed for some reason and did not output any data to stdout.
https://github.com/restic/restic/pull/2139
* Enhancement #2205: Add --ignore-inode option to backup cmd
This option handles backup of virtual filesystems that do not keep fixed inodes for files, like
Fuse-based, pCloud, etc. Ignoring inode changes allows to consider the file as unchanged if
last modification date and size are unchanged.
https://github.com/restic/restic/issues/1631
https://github.com/restic/restic/pull/2205
https://github.com/restic/restic/pull/2047
* Enhancement #2220: Add config option to set S3 storage class
The `s3.storage-class` option can be passed to restic (using `-o`) to specify the storage
class to be used for S3 objects created by restic.
The storage class is passed as-is to S3, so it needs to be understood by the API. On AWS, it can be
one of `STANDARD`, `STANDARD_IA`, `ONEZONE_IA`, `INTELLIGENT_TIERING` and
`REDUCED_REDUNDANCY`. If unspecified, the default storage class is used (`STANDARD` on
AWS).
You can mix storage classes in the same bucket, and the setting isn't stored in the restic
repository, so be sure to specify it with each command that writes to S3.
https://github.com/restic/restic/issues/706
https://github.com/restic/restic/pull/2220
Changelog for restic 0.9.4 (2019-01-06)
=======================================
The following sections list the changes in restic 0.9.4 relevant to
restic users. The changes are ordered by importance.
Summary
-------
* Fix #1989: Google Cloud Storage: Respect bandwidth limit
* Fix #2040: Add host name filter shorthand flag for `stats` command
* Fix #2068: Correctly return error loading data
* Fix #2095: Consistently use local time for snapshots times
* Enh #1605: Concurrent restore
* Enh #2089: Increase granularity of the "keep within" retention policy
* Enh #2097: Add key hinting
* Enh #2017: Mount: Enforce FUSE Unix permissions with allow-other
* Enh #2070: Make all commands display timestamps in local time
* Enh #2085: Allow --files-from to be specified multiple times
* Enh #2094: Run command to get password
Details
-------
* Bugfix #1989: Google Cloud Storage: Respect bandwidth limit
The GCS backend did not respect the bandwidth limit configured, a previous commit
accidentally removed support for it.
https://github.com/restic/restic/issues/1989
https://github.com/restic/restic/pull/2100
* Bugfix #2040: Add host name filter shorthand flag for `stats` command
The default value for `--host` flag was set to 'H' (the shorthand version of the flag), this
caused the lookup for the latest snapshot to fail.
Add shorthand flag `-H` for `--host` (with empty default so if these flags are not specified the
latest snapshot will not filter by host name).
Also add shorthand `-H` for `backup` command.
https://github.com/restic/restic/issues/2040
* Bugfix #2068: Correctly return error loading data
In one case during `prune` and `check`, an error loading data from the backend is not returned
properly. This is now corrected.
https://github.com/restic/restic/issues/1999#issuecomment-433737921
https://github.com/restic/restic/pull/2068
* Bugfix #2095: Consistently use local time for snapshots times
By default snapshots created with restic backup were set to local time, but when the --time flag
was used the provided timestamp was parsed as UTC. With this change all snapshots times are set
to local time.
https://github.com/restic/restic/pull/2095
* Enhancement #1605: Concurrent restore
This change significantly improves restore performance, especially when using
high-latency remote repositories like B2.
The implementation now uses several concurrent threads to download and process multiple
remote files concurrently. To further reduce restore time, each remote file is downloaded
using a single repository request.
https://github.com/restic/restic/issues/1605
https://github.com/restic/restic/pull/1719
* Enhancement #2089: Increase granularity of the "keep within" retention policy
The `keep-within` option of the `forget` command now accepts time ranges with an hourly
granularity. For example, running `restic forget --keep-within 3d12h` will keep all the
snapshots made within three days and twelve hours from the time of the latest snapshot.
https://github.com/restic/restic/issues/2089
https://github.com/restic/restic/pull/2090
* Enhancement #2097: Add key hinting
Added a new option `--key-hint` and corresponding environment variable `RESTIC_KEY_HINT`.
The key hint is a key ID to try decrypting first, before other keys in the repository.
This change will benefit repositories with many keys; if the correct key hint is supplied then
restic only needs to check one key. If the key hint is incorrect (the key does not exist, or the
password is incorrect) then restic will check all keys, as usual.
https://github.com/restic/restic/issues/2097
* Enhancement #2017: Mount: Enforce FUSE Unix permissions with allow-other
The fuse mount (`restic mount`) now lets the kernel check the permissions of the files within
snapshots (this is done through the `DefaultPermissions` FUSE option) when the option
`--allow-other` is specified.
To restore the old behavior, we've added the `--no-default-permissions` option. This allows
all users that have access to the mount point to access all files within the snapshots.
https://github.com/restic/restic/pull/2017
* Enhancement #2070: Make all commands display timestamps in local time
Restic used to drop the timezone information from displayed timestamps, it now converts
timestamps to local time before printing them so the times can be easily compared to.
https://github.com/restic/restic/pull/2070
* Enhancement #2085: Allow --files-from to be specified multiple times
Before, restic took only the last file specified with `--files-from` into account, this is now
corrected.
https://github.com/restic/restic/issues/2085
https://github.com/restic/restic/pull/2086
* Enhancement #2094: Run command to get password
We've added the `--password-command` option which allows specifying a command that restic
runs every time the password for the repository is needed, so it can be integrated with a
password manager or keyring. The option can also be set via the environment variable
`$RESTIC_PASSWORD_COMMAND`.
https://github.com/restic/restic/pull/2094
Changelog for restic 0.9.3 (2018-10-13)
=======================================
The following sections list the changes in restic 0.9.3 relevant to
restic users. The changes are ordered by importance.
Summary
-------
* Fix #1935: Remove truncated files from cache
* Fix #1978: Do not return an error when the scanner is slower than backup
* Enh #1766: Restore: suppress lchown errors when not running as root
* Enh #1909: Reject files/dirs by name first
* Enh #1940: Add directory filter to ls command
* Enh #1967: Use `--host` everywhere
* Enh #2028: Display size of cache directories
* Enh #1777: Improve the `find` command
* Enh #1876: Display reason why forget keeps snapshots
* Enh #1891: Accept glob in paths loaded via --files-from
* Enh #1920: Vendor dependencies with Go 1.11 Modules
* Enh #1949: Add new command `self-update`
* Enh #1953: Ls: Add JSON output support for restic ls cmd
* Enh #1962: Stream JSON output for ls command
Details
-------
* Bugfix #1935: Remove truncated files from cache
When a file in the local cache is truncated, and restic tries to access data beyond the end of the
(cached) file, it used to return an error "EOF". This is now fixed, such truncated files are
removed and the data is fetched directly from the backend.
https://github.com/restic/restic/issues/1935
* Bugfix #1978: Do not return an error when the scanner is slower than backup
When restic makes a backup, there's a background task called "scanner" which collects
information on how many files and directories are to be saved, in order to display progress
information to the user. When the backup finishes faster than the scanner, it is aborted
because the result is not needed any more. This logic contained a bug, where quitting the
scanner process was treated as an error, and caused restic to print an unhelpful error message
("context canceled").
https://github.com/restic/restic/issues/1978
https://github.com/restic/restic/pull/1991
* Enhancement #1766: Restore: suppress lchown errors when not running as root
Like "cp" and "rsync" do, restic now only reports errors for changing the ownership of files
during restore if it is run as root, on non-Windows operating systems. On Windows, the error
is reported as usual.
https://github.com/restic/restic/issues/1766
* Enhancement #1909: Reject files/dirs by name first
The current scanner/archiver code had an architectural limitation: it always ran the
`lstat()` system call on all files and directories before a decision to include/exclude the
file/dir was made. This lead to a lot of unnecessary system calls for items that could have been
rejected by their name or path only.
We've changed the archiver/scanner implementation so that it now first rejects by name/path,
and only runs the system call on the remaining items. This reduces the number of `lstat()`
system calls a lot (depending on the exclude settings).
https://github.com/restic/restic/issues/1909
https://github.com/restic/restic/pull/1912
* Enhancement #1940: Add directory filter to ls command
The ls command can now be filtered by directories, so that only files in the given directories
will be shown. If the --recursive flag is specified, then ls will traverse subfolders and list
their files as well.
It used to be possible to specify multiple snapshots, but that has been replaced by only one
snapshot and the possibility of specifying multiple directories.
Specifying directories constrains the walk, which can significantly speed up the listing.
https://github.com/restic/restic/issues/1940
https://github.com/restic/restic/pull/1941
* Enhancement #1967: Use `--host` everywhere
We now use the flag `--host` for all commands which need a host name, using `--hostname` (e.g.
for `restic backup`) still works, but will print a deprecation warning. Also, add the short
option `-H` where possible.
https://github.com/restic/restic/issues/1967
* Enhancement #2028: Display size of cache directories
The `cache` command now by default shows the size of the individual cache directories. It can be
disabled with `--no-size`.
https://github.com/restic/restic/issues/2028
https://github.com/restic/restic/pull/2033
* Enhancement #1777: Improve the `find` command
We've updated the `find` command to support multiple patterns.
`restic find` is now able to list the snapshots containing a specific tree or blob, or even the
snapshots that contain blobs belonging to a given pack. A list of IDs can be given, as long as they
all have the same type.
The command `find` can also display the pack IDs the blobs belong to, if the `--show-pack-id`
flag is provided.
https://github.com/restic/restic/issues/1777
https://github.com/restic/restic/pull/1780
* Enhancement #1876: Display reason why forget keeps snapshots
We've added a column to the list of snapshots `forget` keeps which details the reasons to keep a
particuliar snapshot. This makes debugging policies for forget much easier. Please remember
to always try things out with `--dry-run`!
https://github.com/restic/restic/pull/1876
* Enhancement #1891: Accept glob in paths loaded via --files-from
Before that, behaviour was different if paths were appended to command line or from a file,
because wild card characters were expanded by shell if appended to command line, but not
expanded if loaded from file.
https://github.com/restic/restic/issues/1891
* Enhancement #1920: Vendor dependencies with Go 1.11 Modules
Until now, we've used `dep` for managing dependencies, we've now switch to using Go modules.
For users this does not change much, only if you want to compile restic without downloading
anything with Go 1.11, then you need to run: `go build -mod=vendor build.go`
https://github.com/restic/restic/pull/1920
* Enhancement #1949: Add new command `self-update`
We have added a new command called `self-update` which downloads the latest released version
of restic from GitHub and replaces the current binary with it. It does not rely on any external
program (so it'll work everywhere), but still verifies the GPG signature using the embedded
GPG public key.
By default, the `self-update` command is hidden behind the `selfupdate` built tag, which is
only set when restic is built using `build.go` (including official releases). The reason for
this is that downstream distributions will then not include the command by default, so users
are encouraged to use the platform-specific distribution mechanism.
https://github.com/restic/restic/pull/1949
* Enhancement #1953: Ls: Add JSON output support for restic ls cmd
We've implemented listing files in the repository with JSON as output, just pass `--json` as an
option to `restic ls`. This makes the output of the command machine readable.
https://github.com/restic/restic/pull/1953
* Enhancement #1962: Stream JSON output for ls command
The `ls` command now supports JSON output with the global `--json` flag, and this change
streams out JSON messages one object at a time rather than en entire array buffered in memory
before encoding. The advantage is it allows large listings to be handled efficiently.
Two message types are printed: snapshots and nodes. A snapshot object will precede node
objects which belong to that snapshot. The `struct_type` field can be used to determine which
kind of message an object is.
https://github.com/restic/restic/pull/1962
Changelog for restic 0.9.2 (2018-08-06)
=======================================
The following sections list the changes in restic 0.9.2 relevant to
restic users. The changes are ordered by importance.
Summary
-------
* Fix #1854: Allow saving files/dirs on different fs with `--one-file-system`
* Fix #1870: Fix restore with --include
* Fix #1880: Use `--cache-dir` argument for `check` command
* Fix #1893: Return error when exclude file cannot be read
* Fix #1861: Fix case-insensitive search with restic find
* Enh #1906: Add support for B2 application keys
* Enh #874: Add stats command to get information about a repository
* Enh #1772: Add restore --verify to verify restored file content
* Enh #1853: Add JSON output support to `restic key list`
* Enh #1477: S3 backend: accept AWS_SESSION_TOKEN
* Enh #1901: Update the Backblaze B2 library
Details
-------
* Bugfix #1854: Allow saving files/dirs on different fs with `--one-file-system`
Restic now allows saving files/dirs on a different file system in a subdir correctly even when
`--one-file-system` is specified.
The first thing the restic archiver code does is to build a tree of the target
files/directories. If it detects that a parent directory is already included (e.g. `restic
backup /foo /foo/bar/baz`), it'll ignore the latter argument.
Without `--one-file-system`, that's perfectly valid: If `/foo` is to be archived, it will
include `/foo/bar/baz`. But with `--one-file-system`, `/foo/bar/baz` may reside on a
different file system, so it won't be included with `/foo`.
https://github.com/restic/restic/issues/1854
https://github.com/restic/restic/pull/1855
* Bugfix #1870: Fix restore with --include
We fixed a bug which prevented restic to restore files with an include filter.
https://github.com/restic/restic/issues/1870
https://github.com/restic/restic/pull/1900
* Bugfix #1880: Use `--cache-dir` argument for `check` command
`check` command now uses a temporary sub-directory of the specified directory if set using the
`--cache-dir` argument. If not set, the cache directory is created in the default temporary
directory as before. In either case a temporary cache is used to ensure the actual repository is
checked (rather than a local copy).
The `--cache-dir` argument was not used by the `check` command, instead a cache directory was
created in the temporary directory.
https://github.com/restic/restic/issues/1880
* Bugfix #1893: Return error when exclude file cannot be read
A bug was found: when multiple exclude files were passed to restic and one of them could not be
read, an error was printed and restic continued, ignoring even the existing exclude files.
Now, an error message is printed and restic aborts when an exclude file cannot be read.
https://github.com/restic/restic/issues/1893
* Bugfix #1861: Fix case-insensitive search with restic find
We've fixed the behavior for `restic find -i PATTERN`, which was broken in v0.9.1.
https://github.com/restic/restic/pull/1861
* Enhancement #1906: Add support for B2 application keys
Restic can now use so-called "application keys" which can be created in the B2 dashboard and
were only introduced recently. In contrast to the "master key", such keys can be restricted to a
specific bucket and/or path.
https://github.com/restic/restic/issues/1906
https://github.com/restic/restic/pull/1914
* Enhancement #874: Add stats command to get information about a repository
https://github.com/restic/restic/issues/874
https://github.com/restic/restic/pull/1729
* Enhancement #1772: Add restore --verify to verify restored file content
Restore will print error message if restored file content does not match expected SHA256
checksum
https://github.com/restic/restic/pull/1772
* Enhancement #1853: Add JSON output support to `restic key list`
This PR enables users to get the output of `restic key list` in JSON in addition to the existing
table format.
https://github.com/restic/restic/pull/1853
* Enhancement #1477: S3 backend: accept AWS_SESSION_TOKEN
Before, it was not possible to use s3 backend with AWS temporary security credentials(with
AWS_SESSION_TOKEN). This change gives higher priority to credentials.EnvAWS credentials
provider.
https://github.com/restic/restic/issues/1477
https://github.com/restic/restic/pull/1479
https://github.com/restic/restic/pull/1647
* Enhancement #1901: Update the Backblaze B2 library
We've updated the library we're using for accessing the Backblaze B2 service to 0.5.0 to
include support for upcoming so-called "application keys". With this feature, you can create
access credentials for B2 which are restricted to e.g. a single bucket or even a sub-directory
of a bucket.
https://github.com/restic/restic/pull/1901
https://github.com/kurin/blazer
Changelog for restic 0.9.1 (2018-06-10)
=======================================

View File

@@ -46,15 +46,12 @@ Remember, the easier it is for us to reproduce the bug, the earlier it will be
corrected!
In addition, you can compile restic with debug support by running
`go run -mod=vendor build.go -tags debug` and instructing it to create a debug
log by setting the environment variable `DEBUG_LOG` to a file, e.g. like this:
`go run build.go -tags debug` and instructing it to create a debug log by
setting the environment variable `DEBUG_LOG` to a file, e.g. like this:
$ export DEBUG_LOG=/tmp/restic-debug.log
$ restic backup ~/work
For Go < 1.11, you need to remove the `-mod=vendor` option from the build
command.
Please be aware that the debug log file will contain potentially sensitive
things like file and directory names, so please either redact it before
uploading it somewhere or post only the parts that are really relevant.
@@ -63,37 +60,9 @@ uploading it somewhere or post only the parts that are really relevant.
Development Environment
=======================
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`.
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
$ cd restic
Then use the `go` tool to build restic:
$ go build ./cmd/restic
$ ./restic version
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).
In order to compile restic with the `go` tool directly, 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
@@ -114,7 +83,12 @@ You can then build restic as follows:
The following commands can be used to run all the tests:
$ go test ./...
$ go test ./cmd/... ./internal/...
The repository contains two 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
[`dep`](https://github.com/golang/dep) tool.
Providing Patches
=================
@@ -133,7 +107,8 @@ down to the following steps:
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.
the previous section. Especially take care to place your forked repository
at the correct path (`src/github.com/restic/restic`) within your `GOPATH`.
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.
@@ -149,14 +124,11 @@ 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 to the file
['CHANGELOG.md'](CHANGELOG.md). 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!
@@ -169,14 +141,13 @@ run
gofmt -w **/*.go
in the project root directory before committing. For each Pull Request, the
formatting is tested with `gofmt` for the latest stable version of Go.
Installing the script `fmt-check` from https://github.com/edsrzf/gofmt-git-hook
locally as a pre-commit hook checks formatting before committing automatically,
just copy this script to `.git/hooks/pre-commit`.
in the project root directory before committing. Installing the script
`fmt-check` from https://github.com/edsrzf/gofmt-git-hook locally as a
pre-commit hook checks formatting before committing automatically, just copy
this script to `.git/hooks/pre-commit`.
For each pull request, several different systems run the integration tests on
Linux, macOS and Windows. We won't merge any code that does not pass all tests
Linux, OS X and Windows. We won't merge any code that does not pass all tests
for all systems, so when a tests fails, try to find out what's wrong and fix
it. If you need help on this, please leave a comment in the pull request, and
we'll be glad to assist. Having a PR with failing integration tests is nothing
@@ -193,7 +164,7 @@ history and triaging bugs much easier.
Git commit messages have a very terse summary in the first line of the commit
message, followed by an empty line, followed by a more verbose description or a
List of changed things. For examples, please refer to the excellent [How to
Write a Git Commit Message](https://chris.beams.io/posts/git-commit/).
Write a Git Commit Message](http://chris.beams.io/posts/git-commit/).
If you change/add multiple different things that aren't related at all, try to
make several smaller commits. This is much easier to review. Using `git add -p`

255
Gopkg.lock generated Normal file
View File

@@ -0,0 +1,255 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "bazil.org/fuse"
packages = [".","fs","fuseutil"]
revision = "371fbbdaa8987b715bdd21d6adc4c9b20155f748"
[[projects]]
name = "cloud.google.com/go"
packages = ["compute/metadata"]
revision = "4b98a6370e36d7a85192e7bad08a4ebd82eac2a8"
version = "v0.20.0"
[[projects]]
name = "github.com/Azure/azure-sdk-for-go"
packages = ["storage","version"]
revision = "56332fec5b308fbb6615fa1af6117394cdba186d"
version = "v15.0.0"
[[projects]]
name = "github.com/Azure/go-autorest"
packages = ["autorest","autorest/adal","autorest/azure","autorest/date"]
revision = "ed4b7f5bf1ec0c9ede1fda2681d96771282f2862"
version = "v10.4.0"
[[projects]]
name = "github.com/cenkalti/backoff"
packages = ["."]
revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e"
version = "v2.0.0"
[[projects]]
name = "github.com/cpuguy83/go-md2man"
packages = ["md2man"]
revision = "20f5889cbdc3c73dbd2862796665e7c465ade7d1"
version = "v1.0.8"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
branch = "master"
name = "github.com/dustin/go-humanize"
packages = ["."]
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
[[projects]]
name = "github.com/elithrar/simple-scrypt"
packages = ["."]
revision = "d150773194090feb6c897805a7bcea8d49544e2c"
version = "v1.3.0"
[[projects]]
name = "github.com/go-ini/ini"
packages = ["."]
revision = "6333e38ac20b8949a8dd68baa3650f4dee8f39f0"
version = "v1.33.0"
[[projects]]
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
name = "github.com/google/go-cmp"
packages = ["cmp","cmp/internal/diff","cmp/internal/function","cmp/internal/value"]
revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97"
version = "v0.1.0"
[[projects]]
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
name = "github.com/juju/ratelimit"
packages = ["."]
revision = "59fac5042749a5afb9af70e813da1dd5474f0167"
version = "1.0.1"
[[projects]]
branch = "master"
name = "github.com/kr/fs"
packages = ["."]
revision = "2788f0dbd16903de03cb8186e5c7d97b69ad387b"
[[projects]]
name = "github.com/kurin/blazer"
packages = ["b2","base","internal/b2assets","internal/b2types","internal/blog","x/window"]
revision = "318e9768bf9a0fe52a64b9f8fe74f4f5caef6452"
version = "v0.4.4"
[[projects]]
name = "github.com/marstr/guid"
packages = ["."]
revision = "8bd9a64bf37eb297b492a4101fb28e80ac0b290f"
version = "v1.1.0"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
name = "github.com/minio/minio-go"
packages = [".","pkg/credentials","pkg/encrypt","pkg/policy","pkg/s3signer","pkg/s3utils","pkg/set"]
revision = "66252c2a3c15f7b90cc8493d497a04ac3b6e3606"
version = "5.0.0"
[[projects]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
[[projects]]
branch = "master"
name = "github.com/ncw/swift"
packages = ["."]
revision = "b2a7479cf26fa841ff90dd932d0221cb5c50782d"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/pkg/profile"
packages = ["."]
revision = "5b67d428864e92711fcbd2f8629456121a56d91f"
version = "v1.2.1"
[[projects]]
name = "github.com/pkg/sftp"
packages = ["."]
revision = "49488377fa2f14143ba3067cf7555f60f6c7b550"
version = "1.5.0"
[[projects]]
name = "github.com/pkg/xattr"
packages = ["."]
revision = "1d7b7ffe7c46974a836eb583b7452f22de1c18cf"
version = "v0.2.3"
[[projects]]
name = "github.com/restic/chunker"
packages = ["."]
revision = "db83917be3b88cc307464b7d8a221c173e34a0db"
version = "v0.2.0"
[[projects]]
name = "github.com/russross/blackfriday"
packages = ["."]
revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5"
version = "v1.5.1"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
name = "github.com/spf13/cobra"
packages = [".","doc"]
revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4"
version = "v0.0.2"
[[projects]]
name = "github.com/spf13/pflag"
packages = ["."]
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["argon2","blake2b","curve25519","ed25519","ed25519/internal/edwards25519","internal/chacha20","pbkdf2","poly1305","scrypt","ssh","ssh/terminal"]
revision = "4ec37c66abab2c7e02ae775328b2ff001c3f025a"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context","context/ctxhttp","http2","http2/hpack","idna","lex/httplex"]
revision = "6078986fec03a1dcc236c34816c71b0e05018fda"
[[projects]]
branch = "master"
name = "golang.org/x/oauth2"
packages = [".","google","internal","jws","jwt"]
revision = "fdc9e635145ae97e6c2cb777c48305600cf515cb"
[[projects]]
branch = "master"
name = "golang.org/x/sync"
packages = ["errgroup"]
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["cpu","unix","windows"]
revision = "7db1c3b1a98089d0071c84f646ff5c96aad43682"
[[projects]]
name = "golang.org/x/text"
packages = ["collate","collate/build","encoding","encoding/internal","encoding/internal/identifier","encoding/unicode","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","internal/utf8internal","language","runes","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
name = "google.golang.org/api"
packages = ["gensupport","googleapi","googleapi/internal/uritemplates","storage/v1"]
revision = "dbbc13f71100fa6ece308335445fca6bb0dd5c2f"
[[projects]]
name = "google.golang.org/appengine"
packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
branch = "v2"
name = "gopkg.in/tomb.v2"
packages = ["."]
revision = "d5d1b5820637886def9eef33e03a27a9f166942c"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "a5de339cba7570216b212439b90e1e6c384c94be8342fe7755b7cb66aa0a3440"
solver-name = "gps-cdcl"
solver-version = 1

21
Gopkg.toml Normal file
View File

@@ -0,0 +1,21 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"

View File

@@ -3,7 +3,7 @@
all: restic
restic:
go run -mod=vendor build.go || go run build.go
go run build.go
clean:
rm -f restic

View File

@@ -1,9 +1,9 @@
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |TestCoverage| |Reviewed by Hound|
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |TestCoverage|
Introduction
------------
restic is a backup program that is fast, efficient and secure. It supports the three major operating systems (Linux, macOS, Windows) and a few smaller ones (FreeBSD, OpenBSD).
restic is a backup program that is fast, efficient and secure.
For detailed usage and installation instructions check out the `documentation <https://restic.readthedocs.io/en/latest>`__.
@@ -111,14 +111,6 @@ License
Restic is licensed under `BSD 2-Clause License <https://opensource.org/licenses/BSD-2-Clause>`__. You can find the
complete text in ``LICENSE``.
Sponsorship
-----------
Backend integration tests for Google Cloud Storage and Microsoft Azure Blob
Storage are sponsored by `AppsCode <https://appscode.com>`__!
|AppsCode|
.. |Documentation| image:: https://readthedocs.org/projects/restic/badge/?version=latest
:target: https://restic.readthedocs.io/en/latest/?badge=latest
.. |Build Status| image:: https://travis-ci.com/restic/restic.svg?branch=master
@@ -129,7 +121,5 @@ Storage are sponsored by `AppsCode <https://appscode.com>`__!
: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
:target: https://houndci.com
.. |TestCoverage| image:: https://codecov.io/gh/restic/restic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/restic/restic

View File

@@ -1 +1 @@
0.9.6
0.9.1

View File

@@ -7,9 +7,6 @@ branches:
only:
- master
cache:
- '%LocalAppData%\go-build'
init:
- ps: >-
$app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'"
@@ -20,8 +17,8 @@ init:
install:
- rmdir c:\go /s /q
- appveyor DownloadFile https://dl.google.com/go/go1.13.4.windows-amd64.msi
- msiexec /i go1.13.4.windows-amd64.msi /q
- appveyor DownloadFile https://dl.google.com/go/go1.10.windows-amd64.msi
- msiexec /i go1.10.windows-amd64.msi /q
- go version
- go env
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip
@@ -29,4 +26,4 @@ install:
- set PATH=bin/;%PATH%
build_script:
- go run -mod=vendor run_integration_tests.go
- go run run_integration_tests.go

326
build.go
View File

@@ -1,18 +1,3 @@
// Description
//
// 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.
//
// For Go < 1.11, it'll create a new GOPATH in a temporary directory, then run
// `go build` on the package configured as Main in the Config struct.
//
// For Go >= 1.11 if the file go.mod is present, it'll use Go modules and not
// setup a GOPATH. It builds the package configured as Main in the Config
// struct with `go build -mod=vendor` to use the vendored dependencies.
// The variable GOPROXY is set to `off` so that no network calls are made. All
// files are copied to a temporary directory before `go build` is called within
// that directory.
// BSD 2-Clause License
//
// Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
@@ -52,6 +37,7 @@ import (
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
@@ -60,22 +46,23 @@ import (
// config contains the configuration for the program to build.
var config = Config{
Name: "restic", // name of the program executable and directory
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
Main: "./cmd/restic", // package name for the main package
DefaultBuildTags: []string{"selfupdate"}, // specify build tags which are always used
Tests: []string{"./..."}, // tests to run
MinVersion: GoVersion{Major: 1, Minor: 10, Patch: 0}, // minimum Go version supported
Name: "restic", // name of the program executable and directory
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
Main: "github.com/restic/restic/cmd/restic", // package name for the main package
Tests: []string{ // tests to run
"github.com/restic/restic/internal/...",
"github.com/restic/restic/cmd/...",
},
MinVersion: GoVersion{Major: 1, Minor: 9, Patch: 0}, // minimum Go version supported
}
// Config configures the build.
type Config struct {
Name string
Namespace string
Main string
DefaultBuildTags []string
Tests []string
MinVersion GoVersion
Name string
Namespace string
Main string
Tests []string
MinVersion GoVersion
}
var (
@@ -84,12 +71,41 @@ var (
runTests bool
enableCGO bool
enablePIE bool
goVersion = ParseGoVersion(runtime.Version())
)
// copy all Go files in src to dst, creating directories on the fly, so calling
// specialDir returns true if the file begins with a special character ('.' or '_').
func specialDir(name string) bool {
if name == "." {
return false
}
base := filepath.Base(name)
if base == "vendor" || base[0] == '_' || base[0] == '.' {
return true
}
return false
}
// excludePath returns true if the file should not be copied to the new GOPATH.
func excludePath(name string) bool {
ext := path.Ext(name)
if ext == ".go" || ext == ".s" || ext == ".h" {
return false
}
parentDir := filepath.Base(filepath.Dir(name))
if parentDir == "testdata" {
return false
}
return true
}
// updateGopath builds a valid GOPATH at dst, with all Go files in src/ copied
// to dst/prefix/, so calling
//
// copy("/tmp/gopath/src/github.com/restic/restic", "/home/u/restic")
// updateGopath("/tmp/gopath", "/home/u/restic", "github.com/restic/restic")
//
// with "/home/u/restic" containing the file "foo.go" yields the following tree
// at "/tmp/gopath":
@@ -100,15 +116,19 @@ var (
// └── restic
// └── restic
// └── foo.go
func copy(dst, src string) error {
verbosePrintf("copy contents of %v to %v\n", src, dst)
func updateGopath(dst, src, prefix string) error {
verbosePrintf("copy contents of %v to %v\n", src, filepath.Join(dst, prefix))
return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
if name == src {
return err
}
if name == ".git" {
return filepath.SkipDir
if specialDir(name) {
if fi.IsDir() {
return filepath.SkipDir
}
return nil
}
if err != nil {
@@ -119,13 +139,17 @@ func copy(dst, src string) error {
return nil
}
if excludePath(name) {
return nil
}
intermediatePath, err := filepath.Rel(src, name)
if err != nil {
return err
}
fileSrc := filepath.Join(src, intermediatePath)
fileDst := filepath.Join(dst, intermediatePath)
fileDst := filepath.Join(dst, "src", prefix, intermediatePath)
return copyFile(fileDst, fileSrc)
})
@@ -140,15 +164,6 @@ func directoryExists(dirname string) bool {
return stat.IsDir()
}
func fileExists(filename string) bool {
stat, err := os.Stat(filename)
if err != nil && os.IsNotExist(err) {
return false
}
return stat.Mode().IsRegular()
}
// copyFile creates dst from src, preserving file attributes and timestamps.
func copyFile(dst, src string) error {
fi, err := os.Stat(src)
@@ -168,34 +183,30 @@ func copyFile(dst, src string) error {
fdst, err := os.Create(dst)
if err != nil {
_ = fsrc.Close()
return err
}
_, err = io.Copy(fdst, fsrc)
if err != nil {
_ = fsrc.Close()
_ = fdst.Close()
if _, err = io.Copy(fdst, fsrc); err != nil {
return err
}
err = fdst.Close()
if err != nil {
_ = fsrc.Close()
return err
if err == nil {
err = fsrc.Close()
}
err = fsrc.Close()
if err != nil {
return err
if err == nil {
err = fdst.Close()
}
err = os.Chmod(dst, fi.Mode())
if err != nil {
return err
if err == nil {
err = os.Chmod(dst, fi.Mode())
}
return os.Chtimes(dst, fi.ModTime(), fi.ModTime())
if err == nil {
err = os.Chtimes(dst, fi.ModTime(), fi.ModTime())
}
return nil
}
// die prints the message with fmt.Fprintf() to stderr and exits with an error
@@ -211,7 +222,7 @@ func showUsage(output io.Writer) {
fmt.Fprintf(output, "OPTIONS:\n")
fmt.Fprintf(output, " -v --verbose output more messages\n")
fmt.Fprintf(output, " -t --tags specify additional build tags\n")
fmt.Fprintf(output, " -k --keep-tempdir do not remove the temporary directory after build\n")
fmt.Fprintf(output, " -k --keep-gopath do not remove the GOPATH after build\n")
fmt.Fprintf(output, " -T --test run tests\n")
fmt.Fprintf(output, " -o --output set output file name\n")
fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n")
@@ -230,20 +241,11 @@ func verbosePrintf(message string, args ...interface{}) {
fmt.Printf("build: "+message, args...)
}
// cleanEnv returns a clean environment with GOPATH, GOBIN and GO111MODULE
// removed (if present).
// cleanEnv returns a clean environment with GOPATH and GOBIN removed (if
// present).
func cleanEnv() (env []string) {
removeKeys := map[string]struct{}{
"GOPATH": struct{}{},
"GOBIN": struct{}{},
"GO111MODULE": struct{}{},
}
for _, v := range os.Environ() {
data := strings.SplitN(v, "=", 2)
name := data[0]
if _, ok := removeKeys[name]; ok {
if strings.HasPrefix(v, "GOPATH=") || strings.HasPrefix(v, "GOBIN=") {
continue
}
@@ -254,17 +256,17 @@ func cleanEnv() (env []string) {
}
// build runs "go build args..." with GOPATH set to gopath.
func build(cwd string, env map[string]string, args ...string) error {
func build(cwd string, ver GoVersion, goos, goarch, goarm, gopath string, args ...string) error {
a := []string{"build"}
if goVersion.AtLeast(GoVersion{1, 10, 0}) {
if ver.AtLeast(GoVersion{1, 10, 0}) {
verbosePrintf("Go version is at least 1.10, using new syntax for -gcflags\n")
// use new prefix
a = append(a, "-asmflags", fmt.Sprintf("all=-trimpath=%s", cwd))
a = append(a, "-gcflags", fmt.Sprintf("all=-trimpath=%s", cwd))
a = append(a, "-asmflags", fmt.Sprintf("all=-trimpath=%s", gopath))
a = append(a, "-gcflags", fmt.Sprintf("all=-trimpath=%s", gopath))
} else {
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", cwd))
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", cwd))
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
}
if enablePIE {
a = append(a, "-buildmode=pie")
@@ -272,9 +274,9 @@ func build(cwd string, env map[string]string, args ...string) error {
a = append(a, args...)
cmd := exec.Command("go", a...)
cmd.Env = append(cleanEnv(), "GOPROXY=off")
for k, v := range env {
cmd.Env = append(cmd.Env, k+"="+v)
cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos)
if goarm != "" {
cmd.Env = append(cmd.Env, "GOARM="+goarm)
}
if !enableCGO {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
@@ -283,30 +285,20 @@ func build(cwd string, env map[string]string, args ...string) error {
cmd.Dir = cwd
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
verbosePrintf("chdir %q\n", cwd)
verbosePrintf("go %q\n", a)
verbosePrintf("go %s\n", a)
return cmd.Run()
}
// test runs "go test args..." with GOPATH set to gopath.
func test(cwd string, env map[string]string, args ...string) error {
args = append([]string{"test", "-count", "1"}, args...)
func test(cwd, gopath string, args ...string) error {
args = append([]string{"test"}, args...)
cmd := exec.Command("go", args...)
cmd.Env = append(cleanEnv(), "GOPROXY=off")
for k, v := range env {
cmd.Env = append(cmd.Env, k+"="+v)
}
if !enableCGO {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
}
cmd.Env = append(cleanEnv(), "GOPATH="+gopath)
cmd.Dir = cwd
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
verbosePrintf("chdir %q\n", cwd)
verbosePrintf("go %q\n", args)
verbosePrintf("go %s\n", args)
return cmd.Run()
}
@@ -454,24 +446,22 @@ func (v GoVersion) String() string {
}
func main() {
if !goVersion.AtLeast(config.MinVersion) {
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion)
ver := ParseGoVersion(runtime.Version())
if !ver.AtLeast(config.MinVersion) {
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", ver, config.MinVersion)
os.Exit(1)
}
buildTags := config.DefaultBuildTags
buildTags := []string{}
skipNext := false
params := os.Args[1:]
goEnv := map[string]string{}
buildEnv := map[string]string{
"GOOS": runtime.GOOS,
"GOARCH": runtime.GOARCH,
"GOARM": "",
}
targetGOOS := runtime.GOOS
targetGOARCH := runtime.GOARCH
targetGOARM := ""
tempdir := ""
gopath := ""
var outputFilename string
@@ -491,13 +481,13 @@ func main() {
die("-t given but no tag specified")
}
skipNext = true
buildTags = append(buildTags, strings.Split(params[i+1], " ")...)
buildTags = strings.Split(params[i+1], " ")
case "-o", "--output":
skipNext = true
outputFilename = params[i+1]
case "--tempdir":
skipNext = true
tempdir = params[i+1]
gopath = params[i+1]
case "-T", "--test":
runTests = true
case "--enable-cgo":
@@ -506,13 +496,13 @@ func main() {
enablePIE = true
case "--goos":
skipNext = true
buildEnv["GOOS"] = params[i+1]
targetGOOS = params[i+1]
case "--goarch":
skipNext = true
buildEnv["GOARCH"] = params[i+1]
targetGOARCH = params[i+1]
case "--goarm":
skipNext = true
buildEnv["GOARM"] = params[i+1]
targetGOARM = params[i+1]
case "-h":
showUsage(os.Stdout)
return
@@ -523,7 +513,12 @@ func main() {
}
}
verbosePrintf("detected Go version %v\n", goVersion)
verbosePrintf("detected Go version %v\n", ver)
if len(buildTags) == 0 {
verbosePrintf("adding build-tag release\n")
buildTags = []string{"release"}
}
for i := range buildTags {
buildTags[i] = strings.TrimSpace(buildTags[i])
@@ -536,16 +531,50 @@ func main() {
die("Getwd(): %v\n", err)
}
if gopath == "" {
gopath, err = ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
if err != nil {
die("TempDir(): %v\n", err)
}
}
verbosePrintf("create GOPATH at %v\n", gopath)
if err = updateGopath(gopath, root, config.Namespace); err != nil {
die("copying files from %v/src to %v/src failed: %v\n", root, gopath, err)
}
vendor := filepath.Join(root, "vendor")
if directoryExists(vendor) {
if err = updateGopath(gopath, vendor, filepath.Join(config.Namespace, "vendor")); err != nil {
die("copying files from %v to %v failed: %v\n", root, gopath, err)
}
}
defer func() {
if !keepGopath {
verbosePrintf("remove %v\n", gopath)
if err = os.RemoveAll(gopath); err != nil {
die("remove GOPATH at %s failed: %v\n", err)
}
} else {
verbosePrintf("leaving temporary GOPATH at %v\n", gopath)
}
}()
if outputFilename == "" {
outputFilename = config.Name
if buildEnv["GOOS"] == "windows" {
if targetGOOS == "windows" {
outputFilename += ".exe"
}
}
cwd, err := os.Getwd()
if err != nil {
die("Getwd() returned %v\n", err)
}
output := outputFilename
if !filepath.IsAbs(output) {
output = filepath.Join(root, output)
output = filepath.Join(cwd, output)
}
version := getVersion()
@@ -556,68 +585,13 @@ func main() {
ldflags := "-s -w " + constants.LDFlags()
verbosePrintf("ldflags: %s\n", ldflags)
var (
buildArgs []string
testArgs []string
)
mainPackage := config.Main
if strings.HasPrefix(mainPackage, config.Namespace) {
mainPackage = strings.Replace(mainPackage, config.Namespace, "./", 1)
}
buildTarget := filepath.FromSlash(mainPackage)
buildCWD := ""
if goVersion.AtLeast(GoVersion{1, 11, 0}) && fileExists("go.mod") {
verbosePrintf("Go >= 1.11 and 'go.mod' found, building with modules\n")
buildCWD = root
buildArgs = append(buildArgs, "-mod=vendor")
testArgs = append(testArgs, "-mod=vendor")
goEnv["GO111MODULE"] = "on"
buildEnv["GO111MODULE"] = "on"
} else {
if tempdir == "" {
tempdir, err = ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
if err != nil {
die("TempDir(): %v\n", err)
}
}
verbosePrintf("Go < 1.11 or 'go.mod' not found, create GOPATH at %v\n", tempdir)
targetdir := filepath.Join(tempdir, "src", filepath.FromSlash(config.Namespace))
if err = copy(targetdir, root); err != nil {
die("copying files from %v to %v/src failed: %v\n", root, tempdir, err)
}
defer func() {
if !keepGopath {
verbosePrintf("remove %v\n", tempdir)
if err = os.RemoveAll(tempdir); err != nil {
die("remove GOPATH at %s failed: %v\n", tempdir, err)
}
} else {
verbosePrintf("leaving temporary GOPATH at %v\n", tempdir)
}
}()
buildCWD = targetdir
goEnv["GOPATH"] = tempdir
buildEnv["GOPATH"] = tempdir
}
verbosePrintf("environment:\n go: %v\n build: %v\n", goEnv, buildEnv)
buildArgs = append(buildArgs,
args := []string{
"-tags", strings.Join(buildTags, " "),
"-ldflags", ldflags,
"-o", output, buildTarget,
)
"-o", output, config.Main,
}
err = build(buildCWD, buildEnv, buildArgs...)
err = build(filepath.Join(gopath, "src"), ver, targetGOOS, targetGOARCH, targetGOARM, gopath, args...)
if err != nil {
die("build failed: %v\n", err)
}
@@ -625,9 +599,7 @@ func main() {
if runTests {
verbosePrintf("running tests\n")
testArgs = append(testArgs, config.Tests...)
err = test(buildCWD, goEnv, testArgs...)
err = test(cwd, gopath, config.Tests...)
if err != nil {
die("running tests failed: %v\n", err)
}

View File

@@ -1,16 +0,0 @@
Bugfix: Allow saving files/dirs on different fs with `--one-file-system`
restic now allows saving files/dirs on a different file system in a subdir
correctly even when `--one-file-system` is specified.
The first thing the restic archiver code does is to build a tree of the target
files/directories. If it detects that a parent directory is already included
(e.g. `restic backup /foo /foo/bar/baz`), it'll ignore the latter argument.
Without `--one-file-system`, that's perfectly valid: If `/foo` is to be
archived, it will include `/foo/bar/baz`. But with `--one-file-system`,
`/foo/bar/baz` may reside on a different file system, so it won't be included
with `/foo`.
https://github.com/restic/restic/issues/1854
https://github.com/restic/restic/pull/1855

View File

@@ -1,6 +0,0 @@
Bugfix: Fix restore with --include
We fixed a bug which prevented restic to restore files with an include filter.
https://github.com/restic/restic/issues/1870
https://github.com/restic/restic/pull/1900

View File

@@ -1,12 +0,0 @@
Bugfix: Use `--cache-dir` argument for `check` command
`check` command now uses a temporary sub-directory of the specified directory
if set using the `--cache-dir` argument. If not set, the cache directory is
created in the default temporary directory as before.
In either case a temporary cache is used to ensure the actual repository is
checked (rather than a local copy).
The `--cache-dir` argument was not used by the `check` command, instead a
cache directory was created in the temporary directory.
https://github.com/restic/restic/issues/1880

View File

@@ -1,8 +0,0 @@
Bugfix: Return error when exclude file cannot be read
A bug was found: when multiple exclude files were passed to restic and one of
them could not be read, an error was printed and restic continued, ignoring
even the existing exclude files. Now, an error message is printed and restic
aborts when an exclude file cannot be read.
https://github.com/restic/restic/issues/1893

View File

@@ -1,8 +0,0 @@
Enhancement: Add support for B2 application keys
Restic can now use so-called "application keys" which can be created in the B2
dashboard and were only introduced recently. In contrast to the "master key",
such keys can be restricted to a specific bucket and/or path.
https://github.com/restic/restic/issues/1906
https://github.com/restic/restic/pull/1914

View File

@@ -1,4 +0,0 @@
Enhancement: Add stats command to get information about a repository
https://github.com/restic/restic/issues/874
https://github.com/restic/restic/pull/1729

View File

@@ -1,6 +0,0 @@
Enhancement: Add restore --verify to verify restored file content
Restore will print error message if restored file content does not match
expected SHA256 checksum
https://github.com/restic/restic/pull/1772

View File

@@ -1,6 +0,0 @@
Enhancement: Add JSON output support to `restic key list`
This PR enables users to get the output of `restic key list` in JSON in addition
to the existing table format.
https://github.com/restic/restic/pull/1853

View File

@@ -1,6 +0,0 @@
Bugfix: Fix case-insensitive search with restic find
We've fixed the behavior for `restic find -i PATTERN`, which was
broken in v0.9.1.
https://github.com/restic/restic/pull/1861

View File

@@ -1,8 +0,0 @@
Enhancement: S3 backend: accept AWS_SESSION_TOKEN
Before, it was not possible to use s3 backend with AWS temporary security credentials(with AWS_SESSION_TOKEN).
This change gives higher priority to credentials.EnvAWS credentials provider.
https://github.com/restic/restic/issues/1477
https://github.com/restic/restic/pull/1479
https://github.com/restic/restic/pull/1647

View File

@@ -1,9 +0,0 @@
Enhancement: Update the Backblaze B2 library
We've updated the library we're using for accessing the Backblaze B2 service to
0.5.0 to include support for upcoming so-called "application keys". With this
feature, you can create access credentials for B2 which are restricted to e.g.
a single bucket or even a sub-directory of a bucket.
https://github.com/restic/restic/pull/1901
https://github.com/kurin/blazer

View File

@@ -1,7 +0,0 @@
Enhancement: restore: suppress lchown errors when not running as root
Like "cp" and "rsync" do, restic now only reports errors for changing
the ownership of files during restore if it is run as root, on non-Windows
operating systems. On Windows, the error is reported as usual.
https://github.com/restic/restic/issues/1766

View File

@@ -1,14 +0,0 @@
Enhancement: Reject files/dirs by name first
The current scanner/archiver code had an architectural limitation: it always
ran the `lstat()` system call on all files and directories before a decision to
include/exclude the file/dir was made. This lead to a lot of unnecessary system
calls for items that could have been rejected by their name or path only.
We've changed the archiver/scanner implementation so that it now first rejects
by name/path, and only runs the system call on the remaining items. This
reduces the number of `lstat()` system calls a lot (depending on the exclude
settings).
https://github.com/restic/restic/issues/1909
https://github.com/restic/restic/pull/1912

View File

@@ -1,8 +0,0 @@
Bugfix: Remove truncated files from cache
When a file in the local cache is truncated, and restic tries to access data
beyond the end of the (cached) file, it used to return an error "EOF". This is
now fixed, such truncated files are removed and the data is fetched directly
from the backend.
https://github.com/restic/restic/issues/1935

View File

@@ -1,15 +0,0 @@
Enhancement: Add directory filter to ls command
The ls command can now be filtered by directories, so that only files in the
given directories will be shown. If the --recursive flag is specified, then
ls will traverse subfolders and list their files as well.
It used to be possible to specify multiple snapshots, but that has been
replaced by only one snapshot and the possibility of specifying multiple
directories.
Specifying directories constrains the walk, which can significantly speed up
the listing.
https://github.com/restic/restic/issues/1940
https://github.com/restic/restic/pull/1941

View File

@@ -1,7 +0,0 @@
Enhancement: Use `--host` everywhere
We now use the flag `--host` for all commands which need a host name, using
`--hostname` (e.g. for `restic backup`) still works, but will print a
deprecation warning. Also, add the short option `-H` where possible.
https://github.com/restic/restic/issues/1967

View File

@@ -1,12 +0,0 @@
Bugfix: Do not return an error when the scanner is slower than backup
When restic makes a backup, there's a background task called "scanner" which
collects information on how many files and directories are to be saved, in
order to display progress information to the user. When the backup finishes
faster than the scanner, it is aborted because the result is not needed any
more. This logic contained a bug, where quitting the scanner process was
treated as an error, and caused restic to print an unhelpful error message
("context canceled").
https://github.com/restic/restic/issues/1978
https://github.com/restic/restic/pull/1991

View File

@@ -1,7 +0,0 @@
Enhancement: Display size of cache directories
The `cache` command now by default shows the size of the individual cache
directories. It can be disabled with `--no-size`.
https://github.com/restic/restic/issues/2028
https://github.com/restic/restic/pull/2033

View File

@@ -1,13 +0,0 @@
Enhancement: Improve the `find` command
We've updated the `find` command to support multiple patterns.
`restic find` is now able to list the snapshots containing a specific tree
or blob, or even the snapshots that contain blobs belonging to a given pack.
A list of IDs can be given, as long as they all have the same type.
The command `find` can also display the pack IDs the blobs belong to, if
the `--show-pack-id` flag is provided.
https://github.com/restic/restic/issues/1777
https://github.com/restic/restic/pull/1780

View File

@@ -1,7 +0,0 @@
Enhancement: Display reason why forget keeps snapshots
We've added a column to the list of snapshots `forget` keeps which details the
reasons to keep a particuliar snapshot. This makes debugging policies for
forget much easier. Please remember to always try things out with `--dry-run`!
https://github.com/restic/restic/pull/1876

View File

@@ -1,7 +0,0 @@
Enhancement: Accept glob in paths loaded via --files-from
Before that, behaviour was different if paths were appended to command line or
from a file, because wild card characters were expanded by shell if appended to
command line, but not expanded if loaded from file.
https://github.com/restic/restic/issues/1891

View File

@@ -1,8 +0,0 @@
Enhancement: Vendor dependencies with Go 1.11 Modules
Until now, we've used `dep` for managing dependencies, we've now switch to
using Go modules. For users this does not change much, only if you want to
compile restic without downloading anything with Go 1.11, then you need to run:
`go build -mod=vendor build.go`
https://github.com/restic/restic/pull/1920

View File

@@ -1,15 +0,0 @@
Enhancement: Add new command `self-update`
We have added a new command called `self-update` which downloads the
latest released version of restic from GitHub and replaces the current
binary with it. It does not rely on any external program (so it'll work
everywhere), but still verifies the GPG signature using the embedded GPG
public key.
By default, the `self-update` command is hidden behind the `selfupdate`
built tag, which is only set when restic is built using `build.go` (including
official releases). The reason for this is that downstream distributions will
then not include the command by default, so users are encouraged to use the
platform-specific distribution mechanism.
https://github.com/restic/restic/pull/1949

View File

@@ -1,7 +0,0 @@
Enhancement: ls: Add JSON output support for restic ls cmd
We've implemented listing files in the repository with JSON as output, just
pass `--json` as an option to `restic ls`. This makes the output of the command
machine readable.
https://github.com/restic/restic/pull/1953

View File

@@ -1,13 +0,0 @@
Enhancement: Stream JSON output for ls command
The `ls` command now supports JSON output with the global `--json`
flag, and this change streams out JSON messages one object at a time
rather than en entire array buffered in memory before encoding. The
advantage is it allows large listings to be handled efficiently.
Two message types are printed: snapshots and nodes. A snapshot
object will precede node objects which belong to that snapshot.
The `struct_type` field can be used to determine which kind of
message an object is.
https://github.com/restic/restic/pull/1962

View File

@@ -1,11 +0,0 @@
Enhancement: Concurrent restore
This change significantly improves restore performance, especially
when using high-latency remote repositories like B2.
The implementation now uses several concurrent threads to download and process
multiple remote files concurrently. To further reduce restore time, each remote
file is downloaded using a single repository request.
https://github.com/restic/restic/issues/1605
https://github.com/restic/restic/pull/1719

View File

@@ -1,7 +0,0 @@
Bugfix: Google Cloud Storage: Respect bandwidth limit
The GCS backend did not respect the bandwidth limit configured, a previous
commit accidentally removed support for it.
https://github.com/restic/restic/issues/1989
https://github.com/restic/restic/pull/2100

View File

@@ -1,11 +0,0 @@
Bugfix: Add host name filter shorthand flag for `stats` command
The default value for `--host` flag was set to 'H' (the shorthand version of
the flag), this caused the lookup for the latest snapshot to fail.
Add shorthand flag `-H` for `--host` (with empty default so if these flags
are not specified the latest snapshot will not filter by host name).
Also add shorthand `-H` for `backup` command.
https://github.com/restic/restic/issues/2040

View File

@@ -1,9 +0,0 @@
Enhancement: increase granularity of the "keep within" retention policy
The `keep-within` option of the `forget` command now accepts time ranges with
an hourly granularity. For example, running `restic forget --keep-within 3d12h`
will keep all the snapshots made within three days and twelve hours from the
time of the latest snapshot.
https://github.com/restic/restic/issues/2089
https://github.com/restic/restic/pull/2090

View File

@@ -1,12 +0,0 @@
Enhancement: Add key hinting
Added a new option `--key-hint` and corresponding environment variable
`RESTIC_KEY_HINT`. The key hint is a key ID to try decrypting first, before
other keys in the repository.
This change will benefit repositories with many keys; if the correct key hint
is supplied then restic only needs to check one key. If the key hint is
incorrect (the key does not exist, or the password is incorrect) then restic
will check all keys, as usual.
https://github.com/restic/restic/issues/2097

View File

@@ -1,11 +0,0 @@
Enhancement: mount: Enforce FUSE Unix permissions with allow-other
The fuse mount (`restic mount`) now lets the kernel check the permissions of
the files within snapshots (this is done through the `DefaultPermissions` FUSE
option) when the option `--allow-other` is specified.
To restore the old behavior, we've added the `--no-default-permissions` option.
This allows all users that have access to the mount point to access all
files within the snapshots.
https://github.com/restic/restic/pull/2017

View File

@@ -1,6 +0,0 @@
Bugfix: Correctly return error loading data
In one case during `prune` and `check`, an error loading data from the backend is not returned properly. This is now corrected.
https://github.com/restic/restic/pull/2068
https://github.com/restic/restic/issues/1999#issuecomment-433737921

View File

@@ -1,7 +0,0 @@
Enhancement: Make all commands display timestamps in local time
Restic used to drop the timezone information from displayed timestamps, it now
converts timestamps to local time before printing them so the times can be
easily compared to.
https://github.com/restic/restic/pull/2070

View File

@@ -1,7 +0,0 @@
Enhancement: Allow --files-from to be specified multiple times
Before, restic took only the last file specified with `--files-from` into
account, this is now corrected.
https://github.com/restic/restic/issues/2085
https://github.com/restic/restic/pull/2086

View File

@@ -1,8 +0,0 @@
Enhancement: Run command to get password
We've added the `--password-command` option which allows specifying a command
that restic runs every time the password for the repository is needed, so it
can be integrated with a password manager or keyring. The option can also be
set via the environment variable `$RESTIC_PASSWORD_COMMAND`.
https://github.com/restic/restic/pull/2094

View File

@@ -1,7 +0,0 @@
Bugfix: consistently use local time for snapshots times
By default snapshots created with restic backup were set to local time,
but when the --time flag was used the provided timestamp was parsed as
UTC. With this change all snapshots times are set to local time.
https://github.com/restic/restic/pull/2095

View File

@@ -1,7 +0,0 @@
Enhancement: Add case insensitive include & exclude options
The backup and restore commands now have --iexclude and --iinclude flags
as case insensitive variants of --exclude and --include.
https://github.com/restic/restic/issues/1895
https://github.com/restic/restic/pull/2032

View File

@@ -1,8 +0,0 @@
Enhancement: Support streaming JSON output for backup
We've added support for getting machine-readable status output during backup,
just pass the flag `--json` for `restic backup` and restic will output a stream
of JSON objects which contain the current progress.
https://github.com/restic/restic/issues/1937
https://github.com/restic/restic/pull/1944

View File

@@ -1,10 +0,0 @@
Bugfix: Return error when no bytes could be read from stdin
We assume that users reading backup data from stdin want to know when no data
could be read, so now restic returns an error when `backup --stdin` is called
but no bytes could be read. Usually, this means that an earlier command in a
pipe has failed. The documentation was amended and now recommends setting the
`pipefail` option (`set -o pipefail`).
https://github.com/restic/restic/pull/2135
https://github.com/restic/restic/pull/2139

View File

@@ -1,8 +0,0 @@
Enhancement: add Openstack application credential auth for Swift
Since Openstack Queens Identity (auth V3) service supports an application
credential auth method. It allows to create a technical account with the
limited roles. This commit adds an application credential authentication
method for the Swift backend.
https://github.com/restic/restic/issues/2155

View File

@@ -1,15 +0,0 @@
Enhancement: Use ctime when checking for file changes
Previously, restic only checked a file's mtime (along with other non-timestamp
metadata) to decide if a file has changed. This could cause restic to not notice
that a file has changed (and therefore continue to store the old version, as
opposed to the modified version) if something edits the file and then resets the
timestamp. Restic now also checks the ctime of files, so any modifications to a
file should be noticed, and the modified file will be backed up. The ctime check
will be disabled if the --ignore-inode flag was given.
If this change causes problems for you, please open an issue, and we can look in
to adding a seperate flag to disable just the ctime check.
https://github.com/restic/restic/issues/2179
https://github.com/restic/restic/pull/2212

View File

@@ -1,3 +0,0 @@
Bugfix: Don't cancel timeout after 30 seconds for self-update
https://github.com/restic/restic/issues/2181

View File

@@ -1,8 +0,0 @@
Enhancement: Add --json support to forget command
The forget command now supports the --json argument, outputting the
information about what is (or would-be) kept and removed from the
repository.
https://github.com/restic/restic/issues/2184
https://github.com/restic/restic/pull/2185

View File

@@ -1,6 +0,0 @@
Bugfix: Fix reading passwords from stdin
Passwords for the `init`, `key add`, and `key passwd` commands can now be read from
non-terminal stdin.
https://github.com/restic/restic/issues/2203

View File

@@ -1,9 +0,0 @@
Bugfix: Don't abort the find command when a tree can't be loaded
Change the find command so that missing trees don't result in a crash.
Instead, the error is logged to the debug log, and the tree ID is displayed
along with the snapshot it belongs to. This makes it possible to recover
repositories that are missing trees by forgetting the snapshots they are used
in.
https://github.com/restic/restic/issues/2224

View File

@@ -1,10 +0,0 @@
Enhancement: Add group-by option to snapshots command
We have added an option to group the output of the snapshots command, similar
to the output of the forget command. The option has been called "--group-by"
and accepts any combination of the values "host", "paths" and "tags", separated
by commas. Default behavior (not specifying --group-by) has not been changed.
We have added support of the grouping to the JSON output.
https://github.com/restic/restic/issues/2037
https://github.com/restic/restic/pull/2087

View File

@@ -1,8 +0,0 @@
Enhancement: Ability to dump folders to tar via stdout
We've added the ability to dump whole folders to stdout via the `dump` command.
Restic now requires at least Go 1.10 due to a limitation of the standard
library for Go <= 1.9.
https://github.com/restic/restic/pull/2124
https://github.com/restic/restic/issues/2123

View File

@@ -1,8 +0,0 @@
Enhancement: Return error if no bytes could be read for `backup --stdin`
When restic is used to backup the output of a program, like `mysqldump | restic
backup --stdin`, it now returns an error if no bytes could be read at all. This
catches the failure case when `mysqldump` failed for some reason and did not
output any data to stdout.
https://github.com/restic/restic/pull/2139

View File

@@ -1,10 +0,0 @@
Enhancement: Add --ignore-inode option to backup cmd
This option handles backup of virtual filesystems that do not keep fixed
inodes for files, like Fuse-based, pCloud, etc. Ignoring inode changes allows
to consider the file as unchanged if last modification date and size
are unchanged.
https://github.com/restic/restic/pull/2205
https://github.com/restic/restic/pull/2047
https://github.com/restic/restic/issues/1631

View File

@@ -1,16 +0,0 @@
Enhancement: Add config option to set S3 storage class
The `s3.storage-class` option can be passed to restic (using `-o`) to
specify the storage class to be used for S3 objects created by restic.
The storage class is passed as-is to S3, so it needs to be understood by
the API. On AWS, it can be one of `STANDARD`, `STANDARD_IA`,
`ONEZONE_IA`, `INTELLIGENT_TIERING` and `REDUCED_REDUNDANCY`. If
unspecified, the default storage class is used (`STANDARD` on AWS).
You can mix storage classes in the same bucket, and the setting isn't
stored in the restic repository, so be sure to specify it with each
command that writes to S3.
https://github.com/restic/restic/pull/2220
https://github.com/restic/restic/issues/706

View File

@@ -1,6 +0,0 @@
Bugfix: Allow absolute path for filename when backing up from stdin
When backing up from stdin, handle directory path for `--stdin-filename`.
This can be used to specify the full path for the backed-up file.
https://github.com/restic/restic/issues/2063

View File

@@ -1,10 +0,0 @@
Bugfix: Save files with invalid timestamps
When restic reads invalid timestamps (year is before 0000 or after 9999) it
refused to read and archive the file. We've changed the behavior and will now
save modified timestamps with the year set to either 0000 or 9999, the rest of
the timestamp stays the same, so the file will be saved (albeit with a bogus
timestamp).
https://github.com/restic/restic/issues/2174
https://github.com/restic/restic/issues/1173

View File

@@ -1,6 +0,0 @@
Bugfix: Read fresh metadata for unmodified files
Restic took all metadata for files which were detected as unmodified, not taking into account changed metadata (ownership, mode). This is now corrected.
https://github.com/restic/restic/issues/2249
https://github.com/restic/restic/pull/2252

View File

@@ -1,7 +0,0 @@
Bugfix: Add upper bound for t in --read-data-subset=n/t
256 is the effective maximum for t, but restic would allow larger
values, leading to strange behavior.
https://github.com/restic/restic/issues/2301
https://github.com/restic/restic/pull/2304

View File

@@ -1,7 +0,0 @@
Enhancement: Allow multiple retries for interactive password input
Restic used to quit if the repository password was typed incorrectly once.
Restic will now ask the user again for the repository password if typed incorrectly.
The user will now get three tries to input the correct password before restic quits.
https://github.com/restic/restic/issues/2306

View File

@@ -1,6 +0,0 @@
Enhancement: Make `--group-by` accept both singular and plural
One can now use the values `host`/`hosts`, `path`/`paths` and
`tag` / `tags` interchangeably in the `--group-by` argument.
https://github.com/restic/restic/issues/2330

View File

@@ -1,8 +0,0 @@
Bugfix: Check errors when loading index files
Restic now checks and handles errors which occur when loading index files, the
missing check leads to odd errors (and a stack trace printed to users) later.
This was reported in the forum.
https://github.com/restic/restic/pull/2321
https://forum.restic.net/t/check-rebuild-index-prune/1848/13

View File

@@ -1,8 +0,0 @@
Enhancement: Add option to configure S3 region
We've added a new option for setting the region when accessing an S3-compatible
service. For some providers, it is required to set this to a valid value. You
can do that either by setting the environment variable `AWS_DEFAULT_REGION` or
using the option `s3.region`, e.g. like this: `-o s3.region="us-east-1"`.
https://github.com/restic/restic/pull/2350

View File

@@ -1,131 +0,0 @@
package main
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
import (
"bytes"
"encoding/binary"
"fmt"
)
const (
aclUserOwner = 0x0001
aclUser = 0x0002
aclGroupOwner = 0x0004
aclGroup = 0x0008
aclMask = 0x0010
aclOthers = 0x0020
)
type aclSID uint64
type aclElem struct {
Tag uint16
Perm uint16
ID uint32
}
type acl struct {
Version uint32
List []aclElement
}
type aclElement struct {
aclSID
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)
}
func (a aclSID) getID() uint32 {
return uint32(a & 0xffffffff)
}
func (a aclSID) String() string {
switch a >> 32 {
case aclUserOwner:
return "user::"
case aclUser:
return fmt.Sprintf("user:%v:", a.getID())
case aclGroupOwner:
return "group::"
case aclGroup:
return fmt.Sprintf("group:%v:", a.getID())
case aclMask:
return "mask::"
case aclOthers:
return "other::"
}
return "?:"
}
func (a aclElement) String() string {
str := ""
if (a.Perm & 4) != 0 {
str += "r"
} else {
str += "-"
}
if (a.Perm & 2) != 0 {
str += "w"
} else {
str += "-"
}
if (a.Perm & 1) != 0 {
str += "x"
} else {
str += "-"
}
return fmt.Sprintf("%v%v", a.aclSID, str)
}
func (a *acl) decode(xattr []byte) {
var elem aclElement
ae := new(aclElem)
nr := bytes.NewReader(xattr)
e := binary.Read(nr, binary.LittleEndian, &a.Version)
if e != nil {
a.Version = 0
return
}
if len(a.List) > 0 {
a.List = a.List[:0]
}
for binary.Read(nr, binary.LittleEndian, ae) == nil {
elem.aclSID = (aclSID(ae.Tag) << 32) | aclSID(ae.ID)
elem.Perm = ae.Perm
a.List = append(a.List, elem)
}
}
func (a *acl) encode() []byte {
buf := new(bytes.Buffer)
ae := new(aclElem)
binary.Write(buf, binary.LittleEndian, &a.Version)
for _, elem := range a.List {
ae.Tag = uint16(elem.getType())
ae.Perm = elem.Perm
ae.ID = elem.getID()
binary.Write(buf, binary.LittleEndian, ae)
}
return buf.Bytes()
}
func (a *acl) String() string {
var finalacl string
for _, acl := range a.List {
finalacl += acl.String() + "\n"
}
return finalacl
}

View File

@@ -1,96 +0,0 @@
package main
import (
"reflect"
"testing"
)
func Test_acl_decode(t *testing.T) {
type args struct {
xattr []byte
}
tests := []struct {
name string
args args
want string
}{
{
name: "decode string",
args: args{
xattr: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255},
},
want: "user::rw-\nuser:0:rwx\nuser:65534:rwx\ngroup::rwx\nmask::rwx\nother::r--\n",
},
{
name: "decode fail",
args: args{
xattr: []byte("abctest"),
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &acl{}
a.decode(tt.args.xattr)
if tt.want != a.String() {
t.Errorf("acl.decode() = %v, want: %v", a.String(), tt.want)
}
})
}
}
func Test_acl_encode(t *testing.T) {
tests := []struct {
name string
want []byte
args []aclElement
}{
{
name: "encode values",
want: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255},
args: []aclElement{
{
aclSID: 8589934591,
Perm: 6,
},
{
aclSID: 8589934592,
Perm: 7,
},
{
aclSID: 8590000126,
Perm: 7,
},
{
aclSID: 21474836479,
Perm: 7,
},
{
aclSID: 73014444031,
Perm: 7,
},
{
aclSID: 141733920767,
Perm: 4,
},
},
},
{
name: "encode fail",
want: []byte{2, 0, 0, 0},
args: []aclElement{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &acl{
Version: 2,
List: tt.args,
}
if got := a.encode(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("acl.encode() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -4,12 +4,8 @@ import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
@@ -25,7 +21,7 @@ import (
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/textfile"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/jsonstatus"
"github.com/restic/restic/internal/ui/config"
"github.com/restic/restic/internal/ui/termstatus"
)
@@ -37,30 +33,31 @@ The "backup" command creates a new snapshot and saves the files and directories
given as the arguments.
`,
PreRun: func(cmd *cobra.Command, args []string) {
if backupOptions.Host == "" {
if backupOptions.Hostname == "" {
hostname, err := os.Hostname()
if err != nil {
debug.Log("os.Hostname() returned err: %v", err)
return
}
backupOptions.Host = hostname
backupOptions.Hostname = hostname
}
},
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
if backupOptions.Stdin {
for _, filename := range backupOptions.FilesFrom {
if filename == "-" {
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
}
}
err := config.ApplyFlags(&backupOptions.Config, cmd.Flags())
if err != nil {
return err
}
if backupOptions.Stdin && backupOptions.FilesFrom == "-" {
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
}
var t tomb.Tomb
term := termstatus.New(globalOptions.stdout, globalOptions.stderr, globalOptions.Quiet)
t.Go(func() error { term.Run(t.Context(globalOptions.ctx)); return nil })
err := runBackup(backupOptions, globalOptions, term, args)
err = runBackup(backupOptions, globalOptions, term, args)
if err != nil {
return err
}
@@ -71,22 +68,21 @@ given as the arguments.
// BackupOptions bundles all options for the backup command.
type BackupOptions struct {
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
Config config.Backup
Parent string
Force bool
ExcludeFiles []string
ExcludeOtherFS bool
ExcludeIfPresent []string
ExcludeCaches bool
Stdin bool
StdinFilename string
Tags []string
Hostname string
FilesFrom string
TimeStamp string
WithAtime bool
}
var backupOptions BackupOptions
@@ -97,24 +93,20 @@ func init() {
f := cmdBackup.Flags()
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)")
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`)
f.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
f.StringArrayVar(&backupOptions.InsensitiveExcludes, "iexclude", nil, "same as `--exclude` but ignores the casing of filenames")
f.StringArrayP("exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
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 http://bford.info/cachedir/spec.html for the Cache Directory Tagging Standard`)
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file`)
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin")
f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
f.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
f.StringVar(&backupOptions.Host, "hostname", "", "set the `hostname` for the snapshot manually")
f.MarkDeprecated("hostname", "use --host")
f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from file (can be combined with file args/can be specified multiple times)")
f.StringVar(&backupOptions.Hostname, "hostname", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)")
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")
}
// filterExisting returns a slice of all existing items, or an error if no
@@ -186,16 +178,12 @@ func readLinesFromFile(filename string) ([]string, error) {
// Check returns an error when an invalid combination of options was set.
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
if gopts.password == "" {
for _, filename := range opts.FilesFrom {
if filename == "-" {
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
}
}
if opts.FilesFrom == "-" && gopts.password == "" {
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
}
if opts.Stdin {
if len(opts.FilesFrom) > 0 {
if opts.FilesFrom != "" {
return errors.Fatal("--stdin and --files-from cannot be used together")
}
@@ -207,34 +195,37 @@ func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
return nil
}
// collectRejectByNameFuncs returns a list of all functions which may reject data
// from being saved in a snapshot based on path only
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectByNameFunc, err error) {
// collectRejectFuncs returns a list of all functions which may reject data
// from being saved in a snapshot
func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, excludes []string, err error) {
// allowed devices
if opts.ExcludeOtherFS {
f, err := rejectByDevice(targets)
if err != nil {
return nil, nil, err
}
fs = append(fs, f)
}
// exclude restic cache
if repo.Cache != nil {
f, err := rejectResticCache(repo)
if err != nil {
return nil, err
return nil, nil, err
}
fs = append(fs, f)
}
excludes = append(excludes, opts.Config.Excludes...)
// add patterns from file
if len(opts.ExcludeFiles) > 0 {
excludes, err := readExcludePatternsFromFiles(opts.ExcludeFiles)
if err != nil {
return nil, err
}
opts.Excludes = append(opts.Excludes, excludes...)
excludes = append(excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...)
}
if len(opts.InsensitiveExcludes) > 0 {
fs = append(fs, rejectByInsensitivePattern(opts.InsensitiveExcludes))
}
if len(opts.Excludes) > 0 {
fs = append(fs, rejectByPattern(opts.Excludes))
if len(excludes) > 0 {
fs = append(fs, rejectByPattern(excludes))
}
if opts.ExcludeCaches {
@@ -244,43 +235,18 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, t
for _, spec := range opts.ExcludeIfPresent {
f, err := rejectIfPresent(spec)
if err != nil {
return nil, err
return nil, nil, err
}
fs = append(fs, f)
}
return fs, nil
}
// collectRejectFuncs returns a list of all functions which may reject data
// from being saved in a snapshot based on path and file info
func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, err error) {
// allowed devices
if opts.ExcludeOtherFS && !opts.Stdin {
f, err := rejectByDevice(targets)
if err != nil {
return nil, err
}
fs = append(fs, f)
}
return fs, nil
return fs, excludes, nil
}
// readExcludePatternsFromFiles reads all exclude files and returns the list of
// exclude patterns. For each line, leading and trailing white space is removed
// and comment lines are ignored. For each remaining pattern, environment
// variables are resolved. For adding a literal dollar sign ($), write $$ to
// the file.
func readExcludePatternsFromFiles(excludeFiles []string) ([]string, error) {
getenvOrDollar := func(s string) string {
if s == "$" {
return "$"
}
return os.Getenv(s)
}
// exclude patterns.
func readExcludePatternsFromFiles(excludeFiles []string) []string {
var excludes []string
for _, filename := range excludeFiles {
err := func() (err error) {
@@ -303,16 +269,17 @@ func readExcludePatternsFromFiles(excludeFiles []string) ([]string, error) {
continue
}
line = os.Expand(line, getenvOrDollar)
line = os.ExpandEnv(line)
excludes = append(excludes, line)
}
return scanner.Err()
}()
if err != nil {
return nil, err
Warnf("error reading exclude patterns: %v:", err)
return nil
}
}
return excludes, nil
return excludes
}
// collectTargets returns a list of target files/dirs from several sources.
@@ -321,31 +288,15 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
return nil, nil
}
var lines []string
for _, file := range opts.FilesFrom {
fromfile, err := readLinesFromFile(file)
if err != nil {
return nil, err
}
// expand wildcards
for _, line := range fromfile {
var expanded []string
expanded, err := filepath.Glob(line)
if err != nil {
return nil, errors.WithMessage(err, fmt.Sprintf("pattern: %s", line))
}
if len(expanded) == 0 {
Warnf("pattern %q does not match any files, skipping\n", line)
}
lines = append(lines, expanded...)
}
fromfile, err := readLinesFromFile(opts.FilesFrom)
if err != nil {
return nil, err
}
// merge files from files-from into normal args so we can reuse the normal
// args checks and have the ability to use both files-from and args at the
// same time
args = append(args, lines...)
args = append(args, fromfile...)
if len(args) == 0 && !opts.Stdin {
return nil, errors.Fatal("nothing to backup, please specify target files/dirs")
}
@@ -374,7 +325,7 @@ func findParentSnapshot(ctx context.Context, repo restic.Repository, opts Backup
// Find last snapshot to set it as parent, if not already set
if !opts.Force && parentID == nil {
id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, opts.Host)
id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, opts.Hostname)
if err == nil {
parentID = &id
} else if err != restic.ErrNoSnapshotFound {
@@ -398,7 +349,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
timeStamp := time.Now()
if opts.TimeStamp != "" {
timeStamp, err = time.ParseInLocation(TimeFormat, opts.TimeStamp, time.Local)
timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp)
if err != nil {
return errors.Fatalf("error in time option: %v\n", err)
}
@@ -406,43 +357,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
var t tomb.Tomb
if gopts.verbosity >= 2 && !gopts.JSON {
term.Print("open repository\n")
}
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
type ArchiveProgressReporter interface {
CompleteItem(item string, previous, current *restic.Node, s archiver.ItemStats, d time.Duration)
StartFile(filename string)
CompleteBlob(filename string, bytes uint64)
ScannerError(item string, fi os.FileInfo, err error) error
ReportTotal(item string, s archiver.ScanStats)
SetMinUpdatePause(d time.Duration)
Run(ctx context.Context) error
Error(item string, fi os.FileInfo, err error) error
Finish(snapshotID restic.ID)
// ui.StdioWrapper
Stdout() io.WriteCloser
Stderr() io.WriteCloser
// ui.Message
E(msg string, args ...interface{})
P(msg string, args ...interface{})
V(msg string, args ...interface{})
VV(msg string, args ...interface{})
}
var p ArchiveProgressReporter
if gopts.JSON {
p = jsonstatus.NewBackup(term, gopts.verbosity)
} else {
p = ui.NewBackup(term, gopts.verbosity)
}
p := ui.NewBackup(term, gopts.verbosity)
// use the terminal for stdout/stderr
prevStdout, prevStderr := gopts.stdout, gopts.stderr
@@ -457,36 +372,32 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
if fps > 60 {
fps = 60
}
p.SetMinUpdatePause(time.Second / time.Duration(fps))
p.MinUpdatePause = time.Second / time.Duration(fps)
}
}
t.Go(func() error { return p.Run(t.Context(gopts.ctx)) })
if !gopts.JSON {
p.V("lock repository")
p.V("open repository")
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
p.V("lock repository")
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, targets)
// rejectFuncs collect functions that can reject items from the backup
rejectFuncs, excludes, err := collectRejectFuncs(opts, repo, targets)
if err != nil {
return err
}
// rejectFuncs collect functions that can reject items from the backup based on path and file info
rejectFuncs, err := collectRejectFuncs(opts, repo, targets)
if err != nil {
return err
}
if !gopts.JSON {
p.V("load index files")
}
p.V("load index files")
err = repo.LoadIndex(gopts.ctx)
if err != nil {
return err
@@ -497,19 +408,10 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
return err
}
if !gopts.JSON && parentSnapshotID != nil {
if parentSnapshotID != nil {
p.V("using parent snapshot %v\n", parentSnapshotID.Str())
}
selectByNameFilter := func(item string) bool {
for _, reject := range rejectByNameFuncs {
if reject(item) {
return false
}
}
return true
}
selectFilter := func(item string, fi os.FileInfo) bool {
for _, reject := range rejectFuncs {
if reject(item, fi) {
@@ -521,63 +423,51 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
var targetFS fs.FS = fs.Local{}
if opts.Stdin {
if !gopts.JSON {
p.V("read data from stdin")
}
filename := path.Join("/", opts.StdinFilename)
p.V("read data from stdin")
targetFS = &fs.Reader{
ModTime: timeStamp,
Name: filename,
Name: opts.StdinFilename,
Mode: 0644,
ReadCloser: os.Stdin,
}
targets = []string{filename}
targets = []string{opts.StdinFilename}
}
sc := archiver.NewScanner(targetFS)
sc.SelectByName = selectByNameFilter
sc.Select = selectFilter
sc.Error = p.ScannerError
sc.Result = p.ReportTotal
if !gopts.JSON {
p.V("start scan on %v", targets)
}
p.V("start scan on %v", targets)
t.Go(func() error { return sc.Scan(t.Context(gopts.ctx), targets) })
arch := archiver.New(repo, targetFS, archiver.Options{})
arch.SelectByName = selectByNameFilter
arch.Select = selectFilter
arch.WithAtime = opts.WithAtime
arch.Error = p.Error
arch.CompleteItem = p.CompleteItem
arch.CompleteItem = p.CompleteItemFn
arch.StartFile = p.StartFile
arch.CompleteBlob = p.CompleteBlob
arch.IgnoreInode = opts.IgnoreInode
if parentSnapshotID == nil {
parentSnapshotID = &restic.ID{}
}
snapshotOpts := archiver.SnapshotOptions{
Excludes: opts.Excludes,
Excludes: excludes,
Tags: opts.Tags,
Time: timeStamp,
Hostname: opts.Host,
Hostname: opts.Hostname,
ParentSnapshot: *parentSnapshotID,
}
uploader := archiver.IndexUploader{
Repository: repo,
Start: func() {
if !gopts.JSON {
p.VV("uploading intermediate index")
}
p.VV("uploading intermediate index")
},
Complete: func(id restic.ID) {
if !gopts.JSON {
p.V("uploaded intermediate index %v", id.Str())
}
p.V("uploaded intermediate index %v", id.Str())
},
}
@@ -585,18 +475,14 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
return uploader.Upload(gopts.ctx, t.Context(gopts.ctx), 30*time.Second)
})
if !gopts.JSON {
p.V("start backup on %v", targets)
}
p.V("start backup on %v", targets)
_, id, err := arch.Snapshot(gopts.ctx, targets, snapshotOpts)
if err != nil {
return errors.Fatalf("unable to save snapshot: %v", err)
}
p.Finish(id)
if !gopts.JSON {
p.P("snapshot %s saved\n", id.Str())
}
p.Finish()
p.P("snapshot %s saved\n", id.Str())
// cleanly shutdown all running goroutines
t.Kill(nil)

View File

@@ -2,7 +2,6 @@ package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"time"
@@ -10,7 +9,6 @@ import (
"github.com/restic/restic/internal/cache"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra"
)
@@ -30,7 +28,6 @@ The "cache" command allows listing and cleaning local cache directories.
type CacheOptions struct {
Cleanup bool
MaxAge uint
NoSize bool
}
var cacheOptions CacheOptions
@@ -41,7 +38,6 @@ func init() {
f := cmdCache.Flags()
f.BoolVar(&cacheOptions.Cleanup, "cleanup", false, "remove old cache directories")
f.UintVar(&cacheOptions.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old")
f.BoolVar(&cacheOptions.NoSize, "no-size", false, "do not output the size of the cache directories")
}
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
@@ -89,22 +85,9 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
return nil
}
tab := table.New()
type data struct {
ID string
Last string
Old string
Size string
}
tab.AddColumn("Repo ID", "{{ .ID }}")
tab.AddColumn("Last Used", "{{ .Last }}")
tab.AddColumn("Old", "{{ .Old }}")
if !opts.NoSize {
tab.AddColumn("Size", "{{ .Size }}")
}
tab := NewTable()
tab.Header = fmt.Sprintf("%-14s %-16s %s", "Repository ID", "Last Used", "Old")
tab.RowFormat = "%-14s %-16s %s"
dirs, err := cache.All(cachedir)
if err != nil {
@@ -126,41 +109,14 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
old = "yes"
}
var size string
if !opts.NoSize {
bytes, err := dirSize(filepath.Join(cachedir, entry.Name()))
if err != nil {
return err
}
size = fmt.Sprintf("%11s", formatBytes(uint64(bytes)))
}
tab.AddRow(data{
tab.Rows = append(tab.Rows, []interface{}{
entry.Name()[:10],
fmt.Sprintf("%d days ago", uint(time.Since(entry.ModTime()).Hours()/24)),
old,
size,
})
}
tab.Write(gopts.stdout)
Printf("%d cache dirs in %s\n", len(dirs), cachedir)
return nil
}
func dirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil || info == nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return nil
})
return size, err
}

View File

@@ -74,7 +74,7 @@ func runCat(gopts GlobalOptions, args []string) error {
fmt.Println(string(buf))
return nil
case "index":
buf, err := repo.LoadAndDecrypt(gopts.ctx, nil, restic.IndexFile, id)
buf, err := repo.LoadAndDecrypt(gopts.ctx, restic.IndexFile, id)
if err != nil {
return err
}
@@ -99,7 +99,7 @@ func runCat(gopts GlobalOptions, args []string) error {
return nil
case "key":
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
buf, err := backend.LoadAll(gopts.ctx, repo.Backend(), h)
if err != nil {
return err
}
@@ -150,7 +150,7 @@ func runCat(gopts GlobalOptions, args []string) error {
switch tpe {
case "pack":
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
buf, err := backend.LoadAll(gopts.ctx, repo.Backend(), h)
if err != nil {
return err
}

View File

@@ -50,7 +50,7 @@ func init() {
f := cmdCheck.Flags()
f.BoolVar(&checkOptions.ReadData, "read-data", false, "read all data blobs")
f.StringVar(&checkOptions.ReadDataSubset, "read-data-subset", "", "read subset n of m data packs (format: `n/m`)")
f.StringVar(&checkOptions.ReadDataSubset, "read-data-subset", "", "read subset of data packs")
f.BoolVar(&checkOptions.CheckUnused, "check-unused", false, "find unused blobs")
f.BoolVar(&checkOptions.WithCache, "with-cache", false, "use the cache")
}
@@ -67,17 +67,11 @@ func checkFlags(opts CheckOptions) error {
if dataSubset[0] == 0 || dataSubset[1] == 0 || dataSubset[0] > dataSubset[1] {
return errors.Fatalf("check flag --read-data-subset=n/t values must be positive integers, and n <= t, e.g. --read-data-subset=1/2")
}
if dataSubset[1] > totalBucketsMax {
return errors.Fatalf("check flag --read-data-subset=n/t t must be at most %d", totalBucketsMax)
}
}
return nil
}
// See doReadData in runCheck below for why this is 256.
const totalBucketsMax = 256
// stringToIntSlice converts string to []uint, using '/' as element separator
func stringToIntSlice(param string) (split []uint, err error) {
if param == "" {
@@ -129,7 +123,6 @@ func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
//
// * if --with-cache is specified, the default cache is used
// * if the user explicitly requested --no-cache, we don't use any cache
// * if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check
// * by default, we use a cache in a temporary directory that is deleted after the check
func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func()) {
cleanup = func() {}
@@ -143,10 +136,8 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func())
return cleanup
}
cachedir := gopts.CacheDir
// use a cache in a temporary directory
tempdir, err := ioutil.TempDir(cachedir, "restic-check-cache-")
tempdir, err := ioutil.TempDir("", "restic-check-cache-")
if err != nil {
// if an error occurs, don't use any cache
Warnf("unable to create temporary directory for cache during check, disabling cache: %v\n", err)
@@ -263,8 +254,6 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
doReadData := func(bucket, totalBuckets uint) {
packs := restic.IDSet{}
for pack := range chkr.GetPacks() {
// If we ever check more than the first byte
// of pack, update totalBucketsMax.
if (uint(pack[0]) % totalBuckets) == (bucket - 1) {
packs.Insert(pack)
}

View File

@@ -21,11 +21,11 @@ The "diff" command shows differences from the first to the second snapshot. The
first characters in each line display what has happened to a particular file or
directory:
* + The item was added
* - The item was removed
* U The metadata (access mode, timestamps, ...) for the item was updated
* M The file's content was modified
* T The type was changed, e.g. a file was made a symlink
+ The item was added
- The item was removed
U The metadata (access mode, timestamps, ...) for the item was updated
M The file's content was modified
T The type was changed, e.g. a file was made a symlink
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {

View File

@@ -1,19 +1,14 @@
package main
import (
"archive/tar"
"context"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/walker"
"github.com/spf13/cobra"
)
@@ -52,20 +47,43 @@ func init() {
flags.StringArrayVar(&dumpOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
}
func splitPath(p string) []string {
d, f := path.Split(p)
if d == "" {
func splitPath(path string) []string {
d, f := filepath.Split(path)
if d == "" || d == "/" {
return []string{f}
}
if d == "/" {
return []string{d}
}
s := splitPath(path.Clean(d))
s := splitPath(filepath.Clean(d))
return append(s, f)
}
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string, pathToPrint string) error {
func dumpNode(ctx context.Context, repo restic.Repository, node *restic.Node) error {
var buf []byte
for _, id := range node.Content {
size, found := repo.LookupBlobSize(id, restic.DataBlob)
if !found {
return errors.Errorf("id %v not found in repository", id)
}
buf = buf[:cap(buf)]
if len(buf) < restic.CiphertextLength(int(size)) {
buf = restic.NewBlobBuffer(int(size))
}
n, err := repo.LoadBlob(ctx, restic.DataBlob, id, buf)
if err != nil {
return err
}
buf = buf[:n]
_, err = os.Stdout.Write(buf)
if err != nil {
return errors.Wrap(err, "Write")
}
}
return nil
}
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string) error {
if tree == nil {
return fmt.Errorf("called with a nil tree")
}
@@ -78,21 +96,18 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repositor
}
item := filepath.Join(prefix, pathComponents[0])
for _, node := range tree.Nodes {
if node.Name == pathComponents[0] || pathComponents[0] == "/" {
if node.Name == pathComponents[0] {
switch {
case l == 1 && node.Type == "file":
return getNodeData(ctx, os.Stdout, repo, node)
return dumpNode(ctx, 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:], pathToPrint)
case node.Type == "dir":
node.Path = pathToPrint
return tarTree(ctx, repo, node, pathToPrint)
return printFromTree(ctx, subtree, repo, item, pathComponents[1:])
case l > 1:
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
return fmt.Errorf("%q should be a dir, but s a %q", item, node.Type)
case node.Type != "file":
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
}
@@ -113,7 +128,7 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
debug.Log("dump file %q from %q", pathToPrint, snapshotIDString)
splittedPath := splitPath(path.Clean(pathToPrint))
splittedPath := splitPath(pathToPrint)
repo, err := OpenRepository(gopts)
if err != nil {
@@ -157,143 +172,10 @@ 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, pathToPrint)
err = printFromTree(ctx, tree, repo, "", splittedPath)
if err != nil {
Exitf(2, "cannot dump file: %v", err)
}
return nil
}
func getNodeData(ctx context.Context, output io.Writer, repo restic.Repository, node *restic.Node) error {
var buf []byte
for _, id := range node.Content {
size, found := repo.LookupBlobSize(id, restic.DataBlob)
if !found {
return errors.Errorf("id %v not found in repository", id)
}
buf = buf[:cap(buf)]
if len(buf) < restic.CiphertextLength(int(size)) {
buf = restic.NewBlobBuffer(int(size))
}
n, err := repo.LoadBlob(ctx, restic.DataBlob, id, buf)
if err != nil {
return err
}
buf = buf[:n]
_, 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

@@ -16,38 +16,27 @@ import (
)
var cmdFind = &cobra.Command{
Use: "find [flags] PATTERN...",
Short: "Find a file, a directory or restic IDs",
Use: "find [flags] PATTERN",
Short: "Find a file or directory",
Long: `
The "find" command searches for files or directories in snapshots stored in the
repo.
It can also be used to search for restic blobs or trees for troubleshooting.`,
Example: `restic find config.json
restic find --json "*.yml" "*.json"
restic find --json --blob 420f620f b46ebe8a ddd38656
restic find --show-pack-id --blob 420f620f
restic find --tree 577c2bc9 f81f2e22 a62827a9
restic find --pack 025c1d06`,
repo. `,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runFind(findOptions, globalOptions, args)
},
}
const shortStr = 8 // Length of short IDs: 4 bytes as hex strings
// FindOptions bundles all options for the find command.
type FindOptions struct {
Oldest string
Newest string
Snapshots []string
BlobID, TreeID bool
PackID, ShowPackID bool
CaseInsensitive bool
ListLong bool
Host string
Paths []string
Tags restic.TagLists
Oldest string
Newest string
Snapshots []string
CaseInsensitive bool
ListLong bool
Host string
Paths []string
Tags restic.TagLists
}
var findOptions FindOptions
@@ -59,10 +48,6 @@ func init() {
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time")
f.StringArrayVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
f.BoolVar(&findOptions.BlobID, "blob", false, "pattern is a blob-ID")
f.BoolVar(&findOptions.TreeID, "tree", false, "pattern is a tree-ID")
f.BoolVar(&findOptions.PackID, "pack", false, "pattern is a pack-ID")
f.BoolVar(&findOptions.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)")
f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
@@ -73,7 +58,7 @@ func init() {
type findPattern struct {
oldest, newest time.Time
pattern []string
pattern string
ignoreCase bool
}
@@ -110,7 +95,7 @@ type statefulOutput struct {
hits int
}
func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
func (s *statefulOutput) PrintJSON(path string, node *restic.Node) {
type findNode restic.Node
b, err := json.Marshal(struct {
// Add these attributes
@@ -154,7 +139,7 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
s.hits++
}
func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) {
func (s *statefulOutput) PrintNormal(path string, node *restic.Node) {
if s.newsn != s.oldsn {
if s.oldsn != nil {
Verbosef("\n")
@@ -165,62 +150,11 @@ func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) {
Printf(formatNode(path, node, s.ListLong) + "\n")
}
func (s *statefulOutput) PrintPattern(path string, node *restic.Node) {
func (s *statefulOutput) Print(path string, node *restic.Node) {
if s.JSON {
s.PrintPatternJSON(path, node)
s.PrintJSON(path, node)
} else {
s.PrintPatternNormal(path, node)
}
}
func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
b, err := json.Marshal(struct {
// Add these attributes
ObjectType string `json:"object_type"`
ID string `json:"id"`
Path string `json:"path"`
ParentTree string `json:"parent_tree,omitempty"`
SnapshotID string `json:"snapshot"`
Time time.Time `json:"time,omitempty"`
}{
ObjectType: kind,
ID: id,
Path: nodepath,
SnapshotID: sn.ID().String(),
ParentTree: treeID,
Time: sn.Time,
})
if err != nil {
Warnf("Marshall failed: %v\n", err)
return
}
if !s.inuse {
Printf("[")
s.inuse = true
}
if s.hits > 0 {
Printf(",")
}
Printf(string(b))
s.hits++
}
func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
Printf("Found %s %s\n", kind, id)
if kind == "blob" {
Printf(" ... in file %s\n", nodepath)
Printf(" (tree %s)\n", treeID)
} else {
Printf(" ... path %s\n", nodepath)
}
Printf(" ... in snapshot %s (%s)\n", sn.ID().Str(), sn.Time.Local().Format(TimeFormat))
}
func (s *statefulOutput) PrintObject(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
if s.JSON {
s.PrintObjectJSON(kind, id, nodepath, treeID, sn)
} else {
s.PrintObjectNormal(kind, id, nodepath, treeID, sn)
s.PrintNormal(path, node)
}
}
@@ -245,9 +179,6 @@ type Finder struct {
pat findPattern
out statefulOutput
ignoreTrees restic.IDSet
blobIDs map[string]struct{}
treeIDs map[string]struct{}
itemsFound int
}
func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error {
@@ -258,35 +189,23 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
}
f.out.newsn = sn
return walker.Walk(ctx, f.repo, *sn.Tree, f.ignoreTrees, func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
return walker.Walk(ctx, f.repo, *sn.Tree, f.ignoreTrees, func(nodepath string, node *restic.Node, err error) (bool, error) {
if err != nil {
debug.Log("Error loading tree %v: %v", parentTreeID, err)
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
return false, walker.SkipNode
return false, err
}
if node == nil {
return false, nil
}
normalizedNodepath := nodepath
name := node.Name
if f.pat.ignoreCase {
normalizedNodepath = strings.ToLower(nodepath)
name = strings.ToLower(name)
}
var foundMatch bool
for _, pat := range f.pat.pattern {
found, err := filter.Match(pat, normalizedNodepath)
if err != nil {
return false, err
}
if found {
foundMatch = true
break
}
foundMatch, err := filter.Match(f.pat.pattern, nodepath)
if err != nil {
return false, err
}
var (
@@ -294,16 +213,9 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
errIfNoMatch error
)
if node.Type == "dir" {
var childMayMatch bool
for _, pat := range f.pat.pattern {
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
if err != nil {
return false, err
}
if mayMatch {
childMayMatch = true
break
}
childMayMatch, err := filter.ChildMatch(f.pat.pattern, nodepath)
if err != nil {
return false, err
}
if !childMayMatch {
@@ -329,171 +241,20 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
}
debug.Log(" found match\n")
f.out.PrintPattern(nodepath, node)
f.out.Print(nodepath, node)
return false, nil
})
}
func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
debug.Log("searching IDs in snapshot %s", sn.ID())
if sn.Tree == nil {
return errors.Errorf("snapshot %v has no tree", sn.ID().Str())
}
f.out.newsn = sn
return walker.Walk(ctx, f.repo, *sn.Tree, f.ignoreTrees, func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
if err != nil {
debug.Log("Error loading tree %v: %v", parentTreeID, err)
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
return false, walker.SkipNode
}
if node == nil {
return false, nil
}
if node.Type == "dir" && f.treeIDs != nil {
treeID := node.Subtree
found := false
if _, ok := f.treeIDs[treeID.Str()]; ok {
found = true
} else if _, ok := f.treeIDs[treeID.String()]; ok {
found = true
}
if found {
f.out.PrintObject("tree", treeID.String(), nodepath, "", sn)
f.itemsFound++
// Terminate if we have found all trees (and we are not
// looking for blobs)
if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil {
// Return an error to terminate the Walk
return true, errors.New("OK")
}
}
}
if node.Type == "file" && f.blobIDs != nil {
for _, id := range node.Content {
idStr := id.String()
if _, ok := f.blobIDs[idStr]; !ok {
// Look for short ID form
if _, ok := f.blobIDs[idStr[:shortStr]]; !ok {
continue
}
// Replace the short ID with the long one
f.blobIDs[idStr] = struct{}{}
delete(f.blobIDs, idStr[:shortStr])
}
f.out.PrintObject("blob", idStr, nodepath, parentTreeID.String(), sn)
break
}
}
return false, nil
})
}
// packsToBlobs converts the list of pack IDs to a list of blob IDs that
// belong to those packs.
func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
packIDs := make(map[string]struct{})
for _, p := range packs {
packIDs[p] = struct{}{}
}
if f.blobIDs == nil {
f.blobIDs = make(map[string]struct{})
}
allPacksFound := false
packsFound := 0
debug.Log("Looking for packs...")
err := f.repo.List(ctx, restic.DataFile, func(id restic.ID, size int64) error {
if allPacksFound {
return nil
}
idStr := id.String()
if _, ok := packIDs[idStr]; !ok {
// Look for short ID form
if _, ok := packIDs[idStr[:shortStr]]; !ok {
return nil
}
}
debug.Log("Found pack %s", idStr)
blobs, _, err := f.repo.ListPack(ctx, id, size)
if err != nil {
return err
}
for _, b := range blobs {
f.blobIDs[b.ID.String()] = struct{}{}
}
// Stop searching when all packs have been found
packsFound++
if packsFound >= len(packIDs) {
allPacksFound = true
}
return nil
})
if err != nil {
return err
}
if !allPacksFound {
return errors.Fatal("unable to find all specified pack(s)")
}
debug.Log("%d blobs found", len(f.blobIDs))
return nil
}
func (f *Finder) findObjectPack(ctx context.Context, id string, t restic.BlobType) {
idx := f.repo.Index()
rid, err := restic.ParseID(id)
if err != nil {
Printf("Note: cannot find pack for object '%s', unable to parse ID: %v\n", id, err)
return
}
blobs, found := idx.Lookup(rid, t)
if !found {
Printf("Object %s not found in the index\n", rid.Str())
return
}
for _, b := range blobs {
if b.ID.Equal(rid) {
Printf("Object belongs to pack %s\n ... Pack %s: %s\n", b.PackID, b.PackID.Str(), b.String())
break
}
}
}
func (f *Finder) findObjectsPacks(ctx context.Context) {
for i := range f.blobIDs {
f.findObjectPack(ctx, i, restic.DataBlob)
}
for i := range f.treeIDs {
f.findObjectPack(ctx, i, restic.TreeBlob)
}
}
func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
if len(args) == 0 {
if len(args) != 1 {
return errors.Fatal("wrong number of arguments")
}
var err error
pat := findPattern{pattern: args}
pat := findPattern{pattern: args[0]}
if opts.CaseInsensitive {
for i := range pat.pattern {
pat.pattern[i] = strings.ToLower(pat.pattern[i])
}
pat.pattern = strings.ToLower(pat.pattern)
pat.ignoreCase = true
}
@@ -509,14 +270,6 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
}
}
// Check at most only one kind of IDs is provided: currently we
// can't mix types
if (opts.BlobID && opts.TreeID) ||
(opts.BlobID && opts.PackID) ||
(opts.TreeID && opts.PackID) {
return errors.Fatal("cannot have several ID types")
}
repo, err := OpenRepository(gopts)
if err != nil {
return err
@@ -543,40 +296,12 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON},
ignoreTrees: restic.NewIDSet(),
}
if opts.BlobID {
f.blobIDs = make(map[string]struct{})
for _, pat := range f.pat.pattern {
f.blobIDs[pat] = struct{}{}
}
}
if opts.TreeID {
f.treeIDs = make(map[string]struct{})
for _, pat := range f.pat.pattern {
f.treeIDs[pat] = struct{}{}
}
}
if opts.PackID {
f.packsToBlobs(ctx, []string{f.pat.pattern[0]}) // TODO: support multiple packs
}
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
if f.blobIDs != nil || f.treeIDs != nil {
if err = f.findIDs(ctx, sn); err != nil && err.Error() != "OK" {
return err
}
continue
}
if err = f.findInSnapshot(ctx, sn); err != nil {
return err
}
}
f.out.Finish()
if opts.ShowPackID && (f.blobIDs != nil || f.treeIDs != nil) {
f.findObjectsPacks(ctx)
}
return nil
}

View File

@@ -3,8 +3,10 @@ package main
import (
"context"
"encoding/json"
"io"
"sort"
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/spf13/cobra"
)
@@ -57,15 +59,14 @@ func init() {
f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots")
f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots")
f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that were created within `duration` before the newest (e.g. 1y5m7d)")
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
// Sadly the commonly used shortcut `H` is already used.
f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname`")
f.MarkDeprecated("hostname", "use --host")
// Deprecated since 2017-03-07.
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname` (deprecated)")
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 format")
@@ -88,129 +89,153 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
return err
}
// group by hostname and dirs
type key struct {
Hostname string
Paths []string
Tags []string
}
snapshotGroups := make(map[string]restic.Snapshots)
var GroupByTag bool
var GroupByHost bool
var GroupByPath bool
var GroupOptionList []string
GroupOptionList = strings.Split(opts.GroupBy, ",")
for _, option := range GroupOptionList {
switch option {
case "host":
GroupByHost = true
case "paths":
GroupByPath = true
case "tags":
GroupByTag = true
case "":
default:
return errors.Fatal("unknown grouping option: '" + option + "'")
}
}
removeSnapshots := 0
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
var snapshots restic.Snapshots
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
snapshots = append(snapshots, sn)
}
if len(args) > 0 {
// When explicit snapshots args are given, remove them immediately.
for _, sn := range snapshots {
if len(args) > 0 {
// When explicit snapshots args are given, remove them immediately.
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())
}
Verbosef("removed snapshot %v\n", sn.ID().Str())
removeSnapshots++
} else {
if !gopts.JSON {
Verbosef("would have removed snapshot %v\n", sn.ID().Str())
}
Verbosef("would have removed snapshot %v\n", sn.ID().Str())
}
}
} else {
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
if err != nil {
return err
}
} else {
// Determining grouping-keys
var tags []string
var hostname string
var paths []string
policy := restic.ExpirePolicy{
Last: opts.Last,
Hourly: opts.Hourly,
Daily: opts.Daily,
Weekly: opts.Weekly,
Monthly: opts.Monthly,
Yearly: opts.Yearly,
Within: opts.Within,
Tags: opts.KeepTags,
}
if policy.Empty() && len(args) == 0 {
if !gopts.JSON {
Verbosef("no policy was specified, no snapshots will be removed\n")
if GroupByTag {
tags = sn.Tags
sort.StringSlice(tags).Sort()
}
}
if !policy.Empty() {
if !gopts.JSON {
Verbosef("Applying Policy: %v\n", policy)
if GroupByHost {
hostname = sn.Hostname
}
if GroupByPath {
paths = sn.Paths
}
var jsonGroups []*ForgetGroup
sort.StringSlice(sn.Paths).Sort()
var k []byte
var err error
for k, snapshotGroup := range snapshotGroups {
if gopts.Verbose >= 1 && !gopts.JSON {
err = PrintSnapshotGroupHeader(gopts.stdout, k)
k, err = json.Marshal(key{Tags: tags, Hostname: hostname, Paths: paths})
if err != nil {
return err
}
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
}
}
policy := restic.ExpirePolicy{
Last: opts.Last,
Hourly: opts.Hourly,
Daily: opts.Daily,
Weekly: opts.Weekly,
Monthly: opts.Monthly,
Yearly: opts.Yearly,
Within: opts.Within,
Tags: opts.KeepTags,
}
if policy.Empty() && len(args) == 0 {
Verbosef("no policy was specified, no snapshots will be removed\n")
}
if !policy.Empty() {
Verbosef("Applying Policy: %v\n", policy)
for k, snapshotGroup := range snapshotGroups {
var key key
if json.Unmarshal([]byte(k), &key) != nil {
return err
}
// Info
Verbosef("snapshots")
var infoStrings []string
if GroupByTag {
infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]")
}
if GroupByHost {
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
}
if GroupByPath {
infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
}
if infoStrings != nil {
Verbosef(" for (" + strings.Join(infoStrings, ", ") + ")")
}
Verbosef(":\n\n")
keep, remove := restic.ApplyPolicy(snapshotGroup, policy)
if len(keep) != 0 && !gopts.Quiet {
Printf("keep %d snapshots:\n", len(keep))
PrintSnapshots(globalOptions.stdout, keep, opts.Compact)
Printf("\n")
}
if len(remove) != 0 && !gopts.Quiet {
Printf("remove %d snapshots:\n", len(remove))
PrintSnapshots(globalOptions.stdout, remove, opts.Compact)
Printf("\n")
}
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
}
}
var key restic.SnapshotGroupKey
if json.Unmarshal([]byte(k), &key) != nil {
return err
}
var fg ForgetGroup
fg.Tags = key.Tags
fg.Host = key.Hostname
fg.Paths = key.Paths
keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
Printf("keep %d snapshots:\n", len(keep))
PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact)
Printf("\n")
}
addJSONSnapshots(&fg.Keep, keep)
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
Printf("remove %d snapshots:\n", len(remove))
PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact)
Printf("\n")
}
addJSONSnapshots(&fg.Remove, remove)
fg.Reasons = reasons
jsonGroups = append(jsonGroups, &fg)
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 removeSnapshots > 0 && opts.Prune {
if !gopts.JSON {
Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
}
Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
if !opts.DryRun {
return pruneRepository(gopts, repo)
}
@@ -218,28 +243,3 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
return nil
}
// ForgetGroup helps to print what is forgotten in JSON.
type ForgetGroup struct {
Tags []string `json:"tags"`
Host string `json:"host"`
Paths []string `json:"paths"`
Keep []Snapshot `json:"keep"`
Remove []Snapshot `json:"remove"`
Reasons []restic.KeepReason `json:"reasons"`
}
func addJSONSnapshots(js *[]Snapshot, list restic.Snapshots) {
for _, sn := range list {
k := Snapshot{
Snapshot: sn,
ID: sn.ID(),
ShortID: sn.ID().Str(),
}
*js = append(*js, k)
}
}
func printJSONForget(stdout io.Writer, forgets []*ForgetGroup) error {
return json.NewEncoder(stdout).Encode(forgets)
}

View File

@@ -12,7 +12,7 @@ var cmdGenerate = &cobra.Command{
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
The "generate" command writes automatically generated files like the man pages
and the auto-completion files for bash and zsh).
`,
DisableAutoGenTag: true,

View File

@@ -2,7 +2,7 @@ package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
@@ -10,7 +10,6 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra"
)
@@ -36,16 +35,10 @@ func init() {
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 {
type keyInfo struct {
Current bool `json:"current"`
ID string `json:"id"`
UserName string `json:"userName"`
HostName string `json:"hostName"`
Created string `json:"created"`
}
var keys []keyInfo
func listKeys(ctx context.Context, s *repository.Repository) error {
tab := NewTable()
tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created")
tab.RowFormat = "%s%-10s %-10s %-10s %s"
err := s.List(ctx, restic.KeyFile, func(id restic.ID, size int64) error {
k, err := repository.LoadKey(ctx, s, id.String())
@@ -54,36 +47,20 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
return nil
}
key := keyInfo{
Current: id.String() == s.KeyName(),
ID: id.Str(),
UserName: k.Username,
HostName: k.Hostname,
Created: k.Created.Local().Format(TimeFormat),
var current string
if id.String() == s.KeyName() {
current = "*"
} else {
current = " "
}
keys = append(keys, key)
tab.Rows = append(tab.Rows, []interface{}{current, id.Str(),
k.Username, k.Hostname, k.Created.Format(TimeFormat)})
return nil
})
if err != nil {
return err
}
if gopts.JSON {
return json.NewEncoder(globalOptions.stdout).Encode(keys)
}
tab := table.New()
tab.AddColumn(" ID", "{{if .Current}}*{{else}} {{end}}{{ .ID }}")
tab.AddColumn("User", "{{ .UserName }}")
tab.AddColumn("Host", "{{ .HostName }}")
tab.AddColumn("Created", "{{ .Created }}")
for _, key := range keys {
tab.AddRow(key)
}
return tab.Write(globalOptions.stdout)
}
@@ -183,7 +160,7 @@ func runKey(gopts GlobalOptions, args []string) error {
return err
}
return listKeys(ctx, repo, gopts)
return listKeys(ctx, repo)
case "add":
lock, err := lockRepo(repo)
defer unlockRepo(lock)

View File

@@ -2,37 +2,21 @@ package main
import (
"context"
"encoding/json"
"os"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/walker"
)
var cmdLs = &cobra.Command{
Use: "ls [flags] [snapshotID] [dir...]",
Use: "ls [flags] [snapshot-ID ...]",
Short: "List files in a snapshot",
Long: `
The "ls" command lists files and directories in a snapshot.
The "ls" command allows listing files and directories in a snapshot.
The special snapshot ID "latest" can be used to list files and
directories of the latest snapshot in the repository. The
--host flag can be used in conjunction to select the latest
snapshot originating from a certain host only.
File listings can optionally be filtered by directories. Any
positional arguments after the snapshot ID are interpreted as
absolute directory paths, and only files inside those directories
will be listed. If the --recursive flag is used, then the filter
will allow traversing into matching directories' subfolders.
Any directory paths specified must be absolute (starting with
a path separator); paths use the forward slash '/' as separator.
The special snapshot-ID "latest" can be used to list files and directories of the latest snapshot in the repository.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -42,11 +26,10 @@ a path separator); paths use the forward slash '/' as separator.
// LsOptions collects all options for the ls command.
type LsOptions struct {
ListLong bool
Host string
Tags restic.TagLists
Paths []string
Recursive bool
ListLong bool
Host string
Tags restic.TagLists
Paths []string
}
var lsOptions LsOptions
@@ -56,31 +39,10 @@ func init() {
flags := cmdLs.Flags()
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
flags.Var(&lsOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given")
flags.StringArrayVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories")
}
type lsSnapshot struct {
*restic.Snapshot
ID *restic.ID `json:"id"`
ShortID string `json:"short_id"`
StructType string `json:"struct_type"` // "snapshot"
}
type lsNode struct {
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
Size uint64 `json:"size,omitempty"`
Mode os.FileMode `json:"mode,omitempty"`
ModTime time.Time `json:"mtime,omitempty"`
AccessTime time.Time `json:"atime,omitempty"`
ChangeTime time.Time `json:"ctime,omitempty"`
StructType string `json:"struct_type"` // "node"
}
func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
@@ -88,51 +50,6 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
}
// extract any specific directories to walk
var dirs []string
if len(args) > 1 {
dirs = args[1:]
for _, dir := range dirs {
if !strings.HasPrefix(dir, "/") {
return errors.Fatal("All path filters must be absolute, starting with a forward slash '/'")
}
}
}
withinDir := func(nodepath string) bool {
if len(dirs) == 0 {
return true
}
for _, dir := range dirs {
// we're within one of the selected dirs, example:
// nodepath: "/test/foo"
// dir: "/test"
if fs.HasPathPrefix(dir, nodepath) {
return true
}
}
return false
}
approachingMatchingTree := func(nodepath string) bool {
if len(dirs) == 0 {
return true
}
for _, dir := range dirs {
// the current node path is a prefix for one of the
// directories, so we're interested in something deeper in the
// tree. Example:
// nodepath: "/test"
// dir: "/test/foo"
if fs.HasPathPrefix(nodepath, dir) {
return true
}
}
return false
}
repo, err := OpenRepository(gopts)
if err != nil {
return err
@@ -144,88 +61,23 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time)
var (
printSnapshot func(sn *restic.Snapshot)
printNode func(path string, node *restic.Node)
)
if gopts.JSON {
enc := json.NewEncoder(gopts.stdout)
printSnapshot = func(sn *restic.Snapshot) {
enc.Encode(lsSnapshot{
Snapshot: sn,
ID: sn.ID(),
ShortID: sn.ID().Str(),
StructType: "snapshot",
})
}
printNode = func(path string, node *restic.Node) {
enc.Encode(lsNode{
Name: node.Name,
Type: node.Type,
Path: path,
UID: node.UID,
GID: node.GID,
Size: node.Size,
Mode: node.Mode,
ModTime: node.ModTime,
AccessTime: node.AccessTime,
ChangeTime: node.ChangeTime,
StructType: "node",
})
}
} else {
printSnapshot = func(sn *restic.Snapshot) {
Verbosef("snapshot %s of %v filtered by %v at %s):\n", sn.ID().Str(), sn.Paths, dirs, sn.Time)
}
printNode = func(path string, node *restic.Node) {
Printf("%s\n", formatNode(path, node, lsOptions.ListLong))
}
}
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) {
printSnapshot(sn)
err := walker.Walk(ctx, repo, *sn.Tree, nil, func(_ restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
err := walker.Walk(ctx, repo, *sn.Tree, nil, func(nodepath string, node *restic.Node, err error) (bool, error) {
if err != nil {
return false, err
}
if node == nil {
return false, nil
}
if withinDir(nodepath) {
// if we're within a dir, print the node
printNode(nodepath, node)
// if recursive listing is requested, signal the walker that it
// should continue walking recursively
if opts.Recursive {
return false, nil
}
}
// if there's an upcoming match deeper in the tree (but we're not
// there yet), signal the walker to descend into any subdirs
if approachingMatchingTree(nodepath) {
return false, nil
}
// otherwise, signal the walker to not walk recursively into any
// subdirs
if node.Type == "dir" {
return false, walker.SkipNode
}
Printf("%s\n", formatNode(nodepath, node, lsOptions.ListLong))
return false, nil
})
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,4 +1,3 @@
// +build !netbsd
// +build !openbsd
// +build !solaris
// +build !windows
@@ -53,14 +52,13 @@ For details please see the documentation for time.Format() at:
// MountOptions collects all options for the mount command.
type MountOptions struct {
OwnerRoot bool
AllowRoot bool
AllowOther bool
NoDefaultPermissions bool
Host string
Tags restic.TagLists
Paths []string
SnapshotTemplate string
OwnerRoot bool
AllowRoot bool
AllowOther bool
Host string
Tags restic.TagLists
Paths []string
SnapshotTemplate string
}
var mountOptions MountOptions
@@ -72,7 +70,6 @@ func init() {
mountFlags.BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
mountFlags.BoolVar(&mountOptions.AllowRoot, "allow-root", false, "allow root user to access the data in the mounted directory")
mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory")
mountFlags.BoolVar(&mountOptions.NoDefaultPermissions, "no-default-permissions", false, "for 'allow-other', ignore Unix permissions and allow users to read all snapshot files")
mountFlags.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`)
mountFlags.Var(&mountOptions.Tags, "tag", "only consider snapshots which include this `taglist`")
@@ -120,11 +117,6 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
if opts.AllowOther {
mountOptions = append(mountOptions, systemFuse.AllowOther())
// let the kernel check permissions unless it is explicitly disabled
if !opts.NoDefaultPermissions {
mountOptions = append(mountOptions, systemFuse.DefaultPermissions())
}
}
c, err := systemFuse.Mount(mountpoint, mountOptions...)
@@ -149,7 +141,7 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
}
Printf("Now serving the repository at %s\n", mountpoint)
Printf("When finished, quit with Ctrl-c or umount the mountpoint.\n")
Printf("Don't forget to umount after quitting!\n")
debug.Log("serving mount at %v", mountpoint)
err = fs.Serve(c, root)

View File

@@ -3,7 +3,7 @@ package main
import (
"fmt"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
"github.com/spf13/cobra"
)

View File

@@ -149,8 +149,8 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
len(idx.Packs), blobs, formatBytes(uint64(stats.bytes)))
blobCount := make(map[restic.BlobHandle]int)
var duplicateBlobs uint64
var duplicateBytes uint64
duplicateBlobs := 0
duplicateBytes := 0
// find duplicate blobs
for _, p := range idx.Packs {
@@ -161,7 +161,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
if blobCount[h] > 1 {
duplicateBlobs++
duplicateBytes += uint64(entry.Length)
duplicateBytes += int(entry.Length)
}
}
}
@@ -252,7 +252,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
continue
}
removeBytes += uint64(blob.Length)
removeBytes += int(blob.Length)
}
if hasActiveBlob {

View File

@@ -1,148 +0,0 @@
package main
import (
"os"
"time"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/spf13/cobra"
)
var cmdRecover = &cobra.Command{
Use: "recover [flags]",
Short: "Recover data from the repository",
Long: `
The "recover" command build a new snapshot from all directories it can find in
the raw data of the repository. It can be used if, for example, a snapshot has
been removed by accident with "forget".
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runRecover(globalOptions)
},
}
func init() {
cmdRoot.AddCommand(cmdRecover)
}
func runRecover(gopts GlobalOptions) error {
hostname, err := os.Hostname()
if err != nil {
return err
}
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
Verbosef("load index files\n")
if err = repo.LoadIndex(gopts.ctx); err != nil {
return err
}
// trees maps a tree ID to whether or not it is referenced by a different
// tree. If it is not referenced, we have a root tree.
trees := make(map[restic.ID]bool)
for blob := range repo.Index().Each(gopts.ctx) {
if blob.Blob.Type != restic.TreeBlob {
continue
}
trees[blob.Blob.ID] = false
}
cur := 0
max := len(trees)
Verbosef("load %d trees\n\n", len(trees))
for id := range trees {
cur++
Verbosef("\rtree (%v/%v)", cur, max)
if !trees[id] {
trees[id] = false
}
tree, err := repo.LoadTree(gopts.ctx, id)
if err != nil {
Warnf("unable to load tree %v: %v\n", id.Str(), err)
continue
}
for _, node := range tree.Nodes {
if node.Type != "dir" || node.Subtree == nil {
continue
}
subtree := *node.Subtree
trees[subtree] = true
}
}
Verbosef("\ndone\n")
roots := restic.NewIDSet()
for id, seen := range trees {
if seen {
continue
}
roots.Insert(id)
}
Verbosef("found %d roots\n", len(roots))
tree := restic.NewTree()
for id := range roots {
var subtreeID = id
node := restic.Node{
Type: "dir",
Name: id.Str(),
Mode: 0755,
Subtree: &subtreeID,
AccessTime: time.Now(),
ModTime: time.Now(),
ChangeTime: time.Now(),
}
tree.Insert(&node)
}
treeID, err := repo.SaveTree(gopts.ctx, tree)
if err != nil {
return errors.Fatalf("unable to save new tree to the repo: %v", err)
}
err = repo.Flush(gopts.ctx)
if err != nil {
return errors.Fatalf("unable to save blobs to the repo: %v", err)
}
err = repo.SaveIndex(gopts.ctx)
if err != nil {
return errors.Fatalf("unable to save new index to the repo: %v", err)
}
sn, err := restic.NewSnapshot([]string{"/recover"}, []string{}, hostname, time.Now())
if err != nil {
return errors.Fatalf("unable to save snapshot: %v", err)
}
sn.Tree = &treeID
id, err := repo.SaveJSONUnpacked(gopts.ctx, restic.SnapshotFile, sn)
if err != nil {
return errors.Fatalf("unable to save snapshot: %v", err)
}
Printf("saved new snapshot %v\n", id.Str())
return nil
}

View File

@@ -5,8 +5,6 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/filter"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/restorer"
"strings"
"github.com/spf13/cobra"
)
@@ -29,15 +27,12 @@ repository.
// RestoreOptions collects all options for the restore command.
type RestoreOptions struct {
Exclude []string
InsensitiveExclude []string
Include []string
InsensitiveInclude []string
Target string
Host string
Paths []string
Tags restic.TagLists
Verify bool
Exclude []string
Include []string
Target string
Host string
Paths []string
Tags restic.TagLists
}
var restoreOptions RestoreOptions
@@ -47,29 +42,16 @@ func init() {
flags := cmdRestore.Flags()
flags.StringArrayVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
flags.StringArrayVar(&restoreOptions.InsensitiveExclude, "iexclude", nil, "same as `--exclude` but ignores the casing of filenames")
flags.StringArrayVarP(&restoreOptions.Include, "include", "i", nil, "include a `pattern`, exclude everything else (can be specified multiple times)")
flags.StringArrayVar(&restoreOptions.InsensitiveInclude, "iinclude", nil, "same as `--include` but ignores the casing of filenames")
flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to")
flags.StringVarP(&restoreOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`)
flags.Var(&restoreOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"")
flags.StringArrayVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
}
func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
ctx := gopts.ctx
hasExcludes := len(opts.Exclude) > 0 || len(opts.InsensitiveExclude) > 0
hasIncludes := len(opts.Include) > 0 || len(opts.InsensitiveInclude) > 0
for i, str := range opts.InsensitiveExclude {
opts.InsensitiveExclude[i] = strings.ToLower(str)
}
for i, str := range opts.InsensitiveInclude {
opts.InsensitiveInclude[i] = strings.ToLower(str)
}
switch {
case len(args) == 0:
@@ -82,7 +64,7 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
return errors.Fatal("please specify a directory to restore to (--target)")
}
if hasExcludes && hasIncludes {
if len(opts.Exclude) > 0 && len(opts.Include) > 0 {
return errors.Fatal("exclude and include patterns are mutually exclusive")
}
@@ -122,14 +104,14 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
}
}
res, err := restorer.NewRestorer(repo, id)
res, err := restic.NewRestorer(repo, id)
if err != nil {
Exitf(2, "creating restorer failed: %v\n", err)
}
totalErrors := 0
res.Error = func(location string, err error) error {
Warnf("ignoring error for %s: %s\n", location, err)
res.Error = func(dir string, node *restic.Node, err error) error {
Warnf("ignoring error for %s: %s\n", dir, err)
totalErrors++
return nil
}
@@ -140,16 +122,11 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
Warnf("error for exclude pattern: %v", err)
}
matchedInsensitive, _, err := filter.List(opts.InsensitiveExclude, strings.ToLower(item))
if err != nil {
Warnf("error for iexclude pattern: %v", err)
}
// An exclude filter is basically a 'wildcard but foo',
// so even if a childMayMatch, other children of a dir may not,
// therefore childMayMatch does not matter, but we should not go down
// unless the dir is selected for restore
selectedForRestore = !matched && !matchedInsensitive
selectedForRestore = !matched
childMayBeSelected = selectedForRestore && node.Type == "dir"
return selectedForRestore, childMayBeSelected
@@ -161,32 +138,21 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
Warnf("error for include pattern: %v", err)
}
matchedInsensitive, childMayMatchInsensitive, err := filter.List(opts.InsensitiveInclude, strings.ToLower(item))
if err != nil {
Warnf("error for iexclude pattern: %v", err)
}
selectedForRestore = matched || matchedInsensitive
childMayBeSelected = (childMayMatch || childMayMatchInsensitive) && node.Type == "dir"
selectedForRestore = matched
childMayBeSelected = childMayMatch && node.Type == "dir"
return selectedForRestore, childMayBeSelected
}
if hasExcludes {
if len(opts.Exclude) > 0 {
res.SelectFilter = selectExcludeFilter
} else if hasIncludes {
} else if len(opts.Include) > 0 {
res.SelectFilter = selectIncludeFilter
}
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
err = res.RestoreTo(ctx, opts.Target)
if err == nil && opts.Verify {
Verbosef("verifying files in %s\n", opts.Target)
var count int
count, err = res.VerifyFiles(ctx, opts.Target)
Verbosef("finished verifying %d files in %s\n", count, opts.Target)
}
if totalErrors > 0 {
Printf("There were %d errors\n", totalErrors)
}

View File

@@ -1,73 +0,0 @@
// xbuild selfupdate
package main
import (
"os"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/selfupdate"
"github.com/spf13/cobra"
)
var cmdSelfUpdate = &cobra.Command{
Use: "self-update [flags]",
Short: "Update the restic binary",
Long: `
The command "self-update" downloads the latest stable release of restic from
GitHub and replaces the currently running binary. After download, the
authenticity of the binary is verified using the GPG signature on the release
files.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runSelfUpdate(selfUpdateOptions, globalOptions, args)
},
}
// SelfUpdateOptions collects all options for the update-restic command.
type SelfUpdateOptions struct {
Output string
}
var selfUpdateOptions SelfUpdateOptions
func init() {
cmdRoot.AddCommand(cmdSelfUpdate)
flags := cmdSelfUpdate.Flags()
flags.StringVar(&selfUpdateOptions.Output, "output", "", "Save the downloaded file as `filename` (default: running binary itself)")
}
func runSelfUpdate(opts SelfUpdateOptions, gopts GlobalOptions, args []string) error {
if opts.Output == "" {
file, err := os.Executable()
if err != nil {
return errors.Wrap(err, "unable to find executable")
}
opts.Output = file
}
fi, err := os.Lstat(opts.Output)
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return errors.Errorf("output file %v is not a normal file, use --output to specify a different file", opts.Output)
}
Printf("writing restic to %v\n", opts.Output)
v, err := selfupdate.DownloadLatestStableRelease(gopts.ctx, opts.Output, version, Verbosef)
if err != nil {
return errors.Fatalf("unable to update restic: %v", err)
}
if v != version {
Printf("successfully updated restic to version %v\n", v)
}
return nil
}

View File

@@ -9,7 +9,6 @@ import (
"strings"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra"
)
@@ -32,7 +31,6 @@ type SnapshotOptions struct {
Paths []string
Compact bool
Last bool
GroupBy string
}
var snapshotOptions SnapshotOptions
@@ -46,7 +44,6 @@ func init() {
f.StringArrayVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)")
f.BoolVarP(&snapshotOptions.Compact, "compact", "c", false, "use compact format")
f.BoolVar(&snapshotOptions.Last, "last", false, "only show the last snapshot for each host and path")
f.StringVarP(&snapshotOptions.GroupBy, "group-by", "g", "", "string for grouping snapshots by host,paths,tags")
}
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
@@ -66,41 +63,25 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
var snapshots restic.Snapshots
var list restic.Snapshots
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
snapshots = append(snapshots, sn)
}
snapshotGroups, grouped, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
if err != nil {
return err
list = append(list, sn)
}
for k, list := range snapshotGroups {
if opts.Last {
list = FilterLastSnapshots(list)
}
sort.Sort(sort.Reverse(list))
snapshotGroups[k] = list
if opts.Last {
list = FilterLastSnapshots(list)
}
sort.Sort(sort.Reverse(list))
if gopts.JSON {
err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, grouped)
err := printSnapshotsJSON(gopts.stdout, list)
if err != nil {
Warnf("error printing snapshots: %v\n", err)
Warnf("error printing snapshot: %v\n", err)
}
return nil
}
for k, list := range snapshotGroups {
if grouped {
err := PrintSnapshotGroupHeader(gopts.stdout, k)
if err != nil {
Warnf("error printing snapshots: %v\n", err)
return nil
}
}
PrintSnapshots(gopts.stdout, list, nil, opts.Compact)
}
PrintSnapshots(gopts.stdout, list, opts.Compact)
return nil
}
@@ -142,16 +123,7 @@ func FilterLastSnapshots(list restic.Snapshots) restic.Snapshots {
}
// PrintSnapshots prints a text table of the snapshots in list to stdout.
func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.KeepReason, compact bool) {
// keep the reasons a snasphot is being kept in a map, so that it doesn't
// get lost when the list of snapshots is sorted
keepReasons := make(map[restic.ID]restic.KeepReason, len(reasons))
if len(reasons) > 0 {
for i, sn := range list {
id := sn.ID()
keepReasons[*id] = reasons[i]
}
}
func PrintSnapshots(stdout io.Writer, list restic.Snapshots, compact bool) {
// always sort the snapshots so that the newer ones are listed last
sort.SliceStable(list, func(i, j int) bool {
@@ -171,112 +143,75 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
}
}
tab := table.New()
if compact {
tab.AddColumn("ID", "{{ .ID }}")
tab.AddColumn("Time", "{{ .Timestamp }}")
tab.AddColumn("Host", "{{ .Hostname }}")
tab.AddColumn("Tags ", `{{ join .Tags "\n" }}`)
tab := NewTable()
if !compact {
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s %-3s %s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags", "", "Directory")
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%%ds %%-3s %%s", -maxHost, -maxTag)
} else {
tab.AddColumn("ID", "{{ .ID }}")
tab.AddColumn("Time", "{{ .Timestamp }}")
tab.AddColumn("Host ", "{{ .Hostname }}")
tab.AddColumn("Tags ", `{{ join .Tags "," }}`)
if len(reasons) > 0 {
tab.AddColumn("Reasons", `{{ join .Reasons "\n" }}`)
}
tab.AddColumn("Paths", `{{ join .Paths "\n" }}`)
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags")
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%s", -maxHost)
}
type snapshot struct {
ID string
Timestamp string
Hostname string
Tags []string
Reasons []string
Paths []string
}
var multiline bool
for _, sn := range list {
data := snapshot{
ID: sn.ID().Str(),
Timestamp: sn.Time.Local().Format(TimeFormat),
Hostname: sn.Hostname,
Tags: sn.Tags,
Paths: sn.Paths,
if len(sn.Paths) == 0 {
continue
}
if len(reasons) > 0 {
id := sn.ID()
data.Reasons = keepReasons[*id].Matches
firstTag := ""
if len(sn.Tags) > 0 {
firstTag = sn.Tags[0]
}
if len(sn.Paths) > 1 && !compact {
multiline = true
rows := len(sn.Paths)
if rows < len(sn.Tags) {
rows = len(sn.Tags)
}
tab.AddRow(data)
}
treeElement := " "
if rows != 1 {
treeElement = "┌──"
}
tab.AddFooter(fmt.Sprintf("%d snapshots", len(list)))
if multiline {
// print an additional blank line between snapshots
var last int
tab.PrintData = func(w io.Writer, idx int, s string) error {
var err error
if idx == last {
_, err = fmt.Fprintf(w, "%s\n", s)
} else {
_, err = fmt.Fprintf(w, "\n%s\n", s)
if !compact {
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, treeElement, sn.Paths[0]})
} else {
allTags := ""
for _, tag := range sn.Tags {
allTags += tag + " "
}
last = idx
return err
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, allTags})
continue
}
if len(sn.Tags) > rows {
rows = len(sn.Tags)
}
for i := 1; i < rows; i++ {
path := ""
if len(sn.Paths) > i {
path = sn.Paths[i]
}
tag := ""
if len(sn.Tags) > i {
tag = sn.Tags[i]
}
treeElement := "│"
if i == (rows - 1) {
treeElement = "└──"
}
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, treeElement, path})
}
}
tab.Footer = fmt.Sprintf("%d snapshots", len(list))
tab.Write(stdout)
}
// PrintSnapshotGroupHeader prints which group of the group-by option the
// following snapshots belong to.
// Prints nothing, if we did not group at all.
func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
var key restic.SnapshotGroupKey
var err error
err = json.Unmarshal([]byte(groupKeyJSON), &key)
if err != nil {
return err
}
if key.Hostname == "" && key.Tags == nil && key.Paths == nil {
return nil
}
// Info
fmt.Fprintf(stdout, "snapshots")
var infoStrings []string
if key.Hostname != "" {
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
}
if key.Tags != nil {
infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]")
}
if key.Paths != nil {
infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
}
if infoStrings != nil {
fmt.Fprintf(stdout, " for (%s)", strings.Join(infoStrings, ", "))
}
fmt.Fprintf(stdout, ":\n")
return nil
}
// Snapshot helps to print Snaphots as JSON with their ID included.
type Snapshot struct {
*restic.Snapshot
@@ -285,58 +220,19 @@ type Snapshot struct {
ShortID string `json:"short_id"`
}
// SnapshotGroup helps to print SnaphotGroups as JSON with their GroupReasons included.
type SnapshotGroup struct {
GroupKey restic.SnapshotGroupKey `json:"group_key"`
Snapshots []Snapshot `json:"snapshots"`
}
// printSnapshotsJSON writes the JSON representation of list to stdout.
func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]restic.Snapshots, grouped bool) error {
if grouped {
var snapshotGroups []SnapshotGroup
func printSnapshotsJSON(stdout io.Writer, list restic.Snapshots) error {
for k, list := range snGroups {
var key restic.SnapshotGroupKey
var err error
var snapshots []Snapshot
err = json.Unmarshal([]byte(k), &key)
if err != nil {
return err
}
for _, sn := range list {
k := Snapshot{
Snapshot: sn,
ID: sn.ID(),
ShortID: sn.ID().Str(),
}
snapshots = append(snapshots, k)
}
group := SnapshotGroup{
GroupKey: key,
Snapshots: snapshots,
}
snapshotGroups = append(snapshotGroups, group)
}
return json.NewEncoder(stdout).Encode(snapshotGroups)
}
// Old behavior
var snapshots []Snapshot
for _, list := range snGroups {
for _, sn := range list {
k := Snapshot{
Snapshot: sn,
ID: sn.ID(),
ShortID: sn.ID().Str(),
}
snapshots = append(snapshots, k)
for _, sn := range list {
k := Snapshot{
Snapshot: sn,
ID: sn.ID(),
ShortID: sn.ID().Str(),
}
snapshots = append(snapshots, k)
}
return json.NewEncoder(stdout).Encode(snapshots)

View File

@@ -1,325 +0,0 @@
package main
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/walker"
"github.com/spf13/cobra"
)
var cmdStats = &cobra.Command{
Use: "stats [flags] [snapshot-ID]",
Short: "Scan the repository and show basic statistics",
Long: `
The "stats" command walks one or all snapshots in a repository and
accumulates statistics about the data stored therein. It reports on
the number of unique files and their sizes, according to one of
the counting modes as given by the --mode flag.
If no snapshot is specified, all snapshots will be considered. Some
modes make more sense over just a single snapshot, while others
are useful across all snapshots, depending on what you are trying
to calculate.
The modes are:
* restore-size: (default) Counts the size of the restored files.
* files-by-contents: Counts total size of files, where a file is
considered unique if it has unique contents.
* raw-data: Counts the size of blobs in the repository, regardless of
how many files reference them.
* blobs-per-file: A combination of files-by-contents and raw-data.
Refer to the online manual for more details about each mode.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runStats(globalOptions, args)
},
}
func init() {
cmdRoot.AddCommand(cmdStats)
f := cmdStats.Flags()
f.StringVar(&countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file, or raw-data")
f.StringVarP(&snapshotByHost, "host", "H", "", "filter latest snapshot by this hostname")
}
func runStats(gopts GlobalOptions, args []string) error {
err := verifyStatsInput(gopts, args)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
if err = repo.LoadIndex(ctx); err != nil {
return err
}
if !gopts.NoLock {
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
}
if !gopts.JSON {
Printf("scanning...\n")
}
// create a container for the stats (and other needed state)
stats := &statsContainer{
uniqueFiles: make(map[fileID]struct{}),
fileBlobs: make(map[string]restic.IDSet),
blobs: restic.NewBlobSet(),
blobsSeen: restic.NewBlobSet(),
}
if snapshotIDString != "" {
// scan just a single snapshot
var sID restic.ID
if snapshotIDString == "latest" {
sID, err = restic.FindLatestSnapshot(ctx, repo, []string{}, []restic.TagList{}, snapshotByHost)
if err != nil {
return errors.Fatalf("latest snapshot for criteria not found: %v", err)
}
} else {
sID, err = restic.FindSnapshot(repo, snapshotIDString)
if err != nil {
return errors.Fatalf("error loading snapshot: %v", err)
}
}
snapshot, err := restic.LoadSnapshot(ctx, repo, sID)
if err != nil {
return errors.Fatalf("error loading snapshot from repo: %v", err)
}
err = statsWalkSnapshot(ctx, snapshot, repo, stats)
} else {
// iterate every snapshot in the repo
err = repo.List(ctx, restic.SnapshotFile, func(snapshotID restic.ID, size int64) error {
snapshot, err := restic.LoadSnapshot(ctx, repo, snapshotID)
if err != nil {
return fmt.Errorf("Error loading snapshot %s: %v", snapshotID.Str(), err)
}
return statsWalkSnapshot(ctx, snapshot, repo, stats)
})
}
if err != nil {
return err
}
if countMode == countModeRawData {
// the blob handles have been collected, but not yet counted
for blobHandle := range stats.blobs {
blobSize, found := repo.LookupBlobSize(blobHandle.ID, blobHandle.Type)
if !found {
return fmt.Errorf("blob %v not found", blobHandle)
}
stats.TotalSize += uint64(blobSize)
stats.TotalBlobCount++
}
}
if gopts.JSON {
err = json.NewEncoder(os.Stdout).Encode(stats)
if err != nil {
return fmt.Errorf("encoding output: %v", err)
}
return nil
}
// inform the user what was scanned and how it was scanned
snapshotsScanned := snapshotIDString
if snapshotsScanned == "latest" {
snapshotsScanned = "the latest snapshot"
} else if snapshotsScanned == "" {
snapshotsScanned = "all snapshots"
}
Printf("Stats for %s in %s mode:\n", snapshotsScanned, countMode)
if stats.TotalBlobCount > 0 {
Printf(" Total Blob Count: %d\n", stats.TotalBlobCount)
}
if stats.TotalFileCount > 0 {
Printf(" Total File Count: %d\n", stats.TotalFileCount)
}
Printf(" Total Size: %-5s\n", formatBytes(stats.TotalSize))
return nil
}
func statsWalkSnapshot(ctx context.Context, snapshot *restic.Snapshot, repo restic.Repository, stats *statsContainer) error {
if snapshot.Tree == nil {
return fmt.Errorf("snapshot %s has nil tree", snapshot.ID().Str())
}
if countMode == countModeRawData {
// count just the sizes of unique blobs; we don't need to walk the tree
// ourselves in this case, since a nifty function does it for us
return restic.FindUsedBlobs(ctx, repo, *snapshot.Tree, stats.blobs, stats.blobsSeen)
}
err := walker.Walk(ctx, repo, *snapshot.Tree, restic.NewIDSet(), statsWalkTree(repo, stats))
if err != nil {
return fmt.Errorf("walking tree %s: %v", *snapshot.Tree, err)
}
return nil
}
func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFunc {
return func(_ restic.ID, npath string, node *restic.Node, nodeErr error) (bool, error) {
if nodeErr != nil {
return true, nodeErr
}
if node == nil {
return true, nil
}
if countMode == countModeUniqueFilesByContents || countMode == countModeBlobsPerFile {
// only count this file if we haven't visited it before
fid := makeFileIDByContents(node)
if _, ok := stats.uniqueFiles[fid]; !ok {
// mark the file as visited
stats.uniqueFiles[fid] = struct{}{}
if countMode == countModeUniqueFilesByContents {
// simply count the size of each unique file (unique by contents only)
stats.TotalSize += node.Size
stats.TotalFileCount++
}
if countMode == countModeBlobsPerFile {
// count the size of each unique blob reference, which is
// by unique file (unique by contents and file path)
for _, blobID := range node.Content {
// ensure we have this file (by path) in our map; in this
// mode, a file is unique by both contents and path
nodePath := filepath.Join(npath, node.Name)
if _, ok := stats.fileBlobs[nodePath]; !ok {
stats.fileBlobs[nodePath] = restic.NewIDSet()
stats.TotalFileCount++
}
if _, ok := stats.fileBlobs[nodePath][blobID]; !ok {
// is always a data blob since we're accessing it via a file's Content array
blobSize, found := repo.LookupBlobSize(blobID, restic.DataBlob)
if !found {
return true, fmt.Errorf("blob %s not found for tree %s", blobID, *node.Subtree)
}
// count the blob's size, then add this blob by this
// file (path) so we don't double-count it
stats.TotalSize += uint64(blobSize)
stats.fileBlobs[nodePath].Insert(blobID)
// this mode also counts total unique blob _references_ per file
stats.TotalBlobCount++
}
}
}
}
}
if countMode == countModeRestoreSize {
// as this is a file in the snapshot, we can simply count its
// size without worrying about uniqueness, since duplicate files
// will still be restored
stats.TotalSize += node.Size
stats.TotalFileCount++
}
return true, nil
}
}
// makeFileIDByContents returns a hash of the blob IDs of the
// node's Content in sequence.
func makeFileIDByContents(node *restic.Node) fileID {
var bb []byte
for _, c := range node.Content {
bb = append(bb, []byte(c[:])...)
}
return sha256.Sum256(bb)
}
func verifyStatsInput(gopts GlobalOptions, args []string) error {
// require a recognized counting mode
switch countMode {
case countModeRestoreSize:
case countModeUniqueFilesByContents:
case countModeBlobsPerFile:
case countModeRawData:
default:
return fmt.Errorf("unknown counting mode: %s (use the -h flag to get a list of supported modes)", countMode)
}
// ensure at most one snapshot was specified
if len(args) > 1 {
return fmt.Errorf("only one snapshot may be specified")
}
// if a snapshot was specified, mark it as the one to scan
if len(args) == 1 {
snapshotIDString = args[0]
}
return nil
}
// statsContainer holds information during a walk of a repository
// to collect information about it, as well as state needed
// for a successful and efficient walk.
type statsContainer struct {
TotalSize uint64 `json:"total_size"`
TotalFileCount uint64 `json:"total_file_count"`
TotalBlobCount uint64 `json:"total_blob_count,omitempty"`
// uniqueFiles marks visited files according to their
// contents (hashed sequence of content blob IDs)
uniqueFiles map[fileID]struct{}
// fileBlobs maps a file name (path) to the set of
// blobs that have been seen as a part of the file
fileBlobs map[string]restic.IDSet
// blobs and blobsSeen are used to count individual
// unique blobs, independent of references to files
blobs, blobsSeen restic.BlobSet
}
// fileID is a 256-bit hash that distinguishes unique files.
type fileID [32]byte
var (
// the mode of counting to perform
countMode string
// the snapshot to scan, as given by the user
snapshotIDString string
// snapshotByHost is the host to filter latest
// snapshot by, if given by user
snapshotByHost string
)
const (
countModeRestoreSize = "restore-size"
countModeUniqueFilesByContents = "files-by-contents"
countModeBlobsPerFile = "blobs-per-file"
countModeRawData = "raw-data"
)

View File

@@ -60,20 +60,15 @@ func (rc *rejectionCache) Store(dir string, rejected bool) {
rc.m[dir] = rejected
}
// RejectByNameFunc is a function that takes a filename of a
// file that would be included in the backup. The function returns true if it
// should be excluded (rejected) from the backup.
type RejectByNameFunc func(path string) bool
// RejectFunc is a function that takes a filename and os.FileInfo of a
// file that would be included in the backup. The function returns true if it
// should be excluded (rejected) from the backup.
type RejectFunc func(path string, fi os.FileInfo) bool
// rejectByPattern returns a RejectByNameFunc which rejects files that match
// rejectByPattern returns a RejectFunc which rejects files that match
// one of the patterns.
func rejectByPattern(patterns []string) RejectByNameFunc {
return func(item string) bool {
func rejectByPattern(patterns []string) RejectFunc {
return func(item string, fi os.FileInfo) bool {
matched, _, err := filter.List(patterns, item)
if err != nil {
Warnf("error for exclude pattern: %v", err)
@@ -88,26 +83,14 @@ func rejectByPattern(patterns []string) RejectByNameFunc {
}
}
// Same as `rejectByPattern` but case insensitive.
func rejectByInsensitivePattern(patterns []string) RejectByNameFunc {
for index, path := range patterns {
patterns[index] = strings.ToLower(path)
}
rejFunc := rejectByPattern(patterns)
return func(item string) bool {
return rejFunc(strings.ToLower(item))
}
}
// rejectIfPresent returns a RejectByNameFunc which itself returns whether a path
// should be excluded. The RejectByNameFunc considers a file to be excluded when
// rejectIfPresent returns a RejectFunc which itself returns whether a path
// should be excluded. The RejectFunc considers a file to be excluded when
// it resides in a directory with an exclusion file, that is specified by
// excludeFileSpec in the form "filename[:content]". The returned error is
// non-nil if the filename component of excludeFileSpec is empty. If rc is
// non-nil, it is going to be used in the RejectByNameFunc to expedite the evaluation
// non-nil, it is going to be used in the RejectFunc to expedite the evaluation
// of a directory based on previous visits.
func rejectIfPresent(excludeFileSpec string) (RejectByNameFunc, error) {
func rejectIfPresent(excludeFileSpec string) (RejectFunc, error) {
if excludeFileSpec == "" {
return nil, errors.New("name for exclusion tagfile is empty")
}
@@ -124,7 +107,7 @@ func rejectIfPresent(excludeFileSpec string) (RejectByNameFunc, error) {
}
debug.Log("using %q as exclusion tagfile", tf)
rc := &rejectionCache{}
fn := func(filename string) bool {
fn := func(filename string, _ os.FileInfo) bool {
return isExcludedByFile(filename, tf, tc, rc)
}
return fn, nil
@@ -269,11 +252,11 @@ func rejectByDevice(samples []string) (RejectFunc, error) {
}, nil
}
// rejectResticCache returns a RejectByNameFunc that rejects the restic cache
// rejectResticCache returns a RejectFunc that rejects the restic cache
// directory (if set).
func rejectResticCache(repo *repository.Repository) (RejectByNameFunc, error) {
func rejectResticCache(repo *repository.Repository) (RejectFunc, error) {
if repo.Cache == nil {
return func(string) bool {
return func(string, os.FileInfo) bool {
return false
}, nil
}
@@ -283,7 +266,7 @@ func rejectResticCache(repo *repository.Repository) (RejectByNameFunc, error) {
return nil, errors.New("cacheBase is empty string")
}
return func(item string) bool {
return func(item string, _ os.FileInfo) bool {
if fs.HasPathPrefix(cacheBase, item) {
debug.Log("rejecting restic cache directory %v", item)
return true

View File

@@ -27,34 +27,7 @@ func TestRejectByPattern(t *testing.T) {
for _, tc := range tests {
t.Run("", func(t *testing.T) {
reject := rejectByPattern(patterns)
res := reject(tc.filename)
if res != tc.reject {
t.Fatalf("wrong result for filename %v: want %v, got %v",
tc.filename, tc.reject, res)
}
})
}
}
func TestRejectByInsensitivePattern(t *testing.T) {
var tests = []struct {
filename string
reject bool
}{
{filename: "/home/user/foo.GO", reject: true},
{filename: "/home/user/foo.c", reject: false},
{filename: "/home/user/foobar", reject: false},
{filename: "/home/user/FOObar/x", reject: true},
{filename: "/home/user/README", reject: false},
{filename: "/home/user/readme.md", reject: true},
}
patterns := []string{"*.go", "README.md", "/home/user/foobar/*"}
for _, tc := range tests {
t.Run("", func(t *testing.T) {
reject := rejectByInsensitivePattern(patterns)
res := reject(tc.filename)
res := reject(tc.filename, nil)
if res != tc.reject {
t.Fatalf("wrong result for filename %v: want %v, got %v",
tc.filename, tc.reject, res)
@@ -167,8 +140,8 @@ func TestMultipleIsExcludedByFile(t *testing.T) {
if err != nil {
return err
}
excludedByFoo := fooExclude(p)
excludedByBar := barExclude(p)
excludedByFoo := fooExclude(p, fi)
excludedByBar := barExclude(p, fi)
excluded := excludedByFoo || excludedByBar
// the log message helps debugging in case the test fails
t.Logf("%q: %v || %v = %v", p, excludedByFoo, excludedByBar, excluded)

View File

@@ -21,7 +21,7 @@ func formatBytes(c uint64) string {
case c > 1<<10:
return fmt.Sprintf("%.3f KiB", b/(1<<10))
default:
return fmt.Sprintf("%d B", c)
return fmt.Sprintf("%dB", c)
}
}
@@ -90,6 +90,6 @@ func formatNode(path string, n *restic.Node, long bool) string {
return fmt.Sprintf("%s %5d %5d %6d %s %s%s",
mode|n.Mode, n.UID, n.GID, n.Size,
n.ModTime.Local().Format(TimeFormat), path,
n.ModTime.Format(TimeFormat), path,
target)
}

View File

@@ -1,7 +1,6 @@
package main
import (
"bufio"
"context"
"fmt"
"io"
@@ -27,38 +26,32 @@ import (
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/limiter"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/textfile"
"github.com/restic/restic/internal/ui/config"
"github.com/restic/restic/internal/ui/options"
"github.com/restic/restic/internal/errors"
"os/exec"
"golang.org/x/crypto/ssh/terminal"
)
var version = "0.9.6"
// TimeFormat is the format used for all timestamps printed by restic.
const TimeFormat = "2006-01-02 15:04:05"
var version = "compiled manually"
// GlobalOptions hold all global options for restic.
type GlobalOptions struct {
Repo string
PasswordFile string
PasswordCommand string
KeyHint string
Quiet bool
Verbose int
NoLock bool
JSON bool
CacheDir string
NoCache bool
CACerts []string
TLSClientCert string
CleanupCache bool
config.Config
Quiet bool
Verbose int
NoLock bool
JSON bool
CacheDir string
NoCache bool
CACerts []string
TLSClientCert string
CleanupCache bool
LimitUploadKb int
LimitDownloadKb int
@@ -72,7 +65,7 @@ type GlobalOptions struct {
// 0 means: don't print any messages except errors, this is used when --quiet is specified
// 1 is the default: print essential messages
// 2 means: print more messages, report minor things, this is used when --verbose is specified
// 3 means: print very detailed debug messages, this is used when --verbose 2 is specified
// 3 means: print very detailed debug messages, this is used when --debug is specified
verbosity uint
Options []string
@@ -94,15 +87,16 @@ func init() {
})
f := cmdRoot.PersistentFlags()
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "specify a shell command to obtain a password (default: $RESTIC_PASSWORD_COMMAND)")
// these fields are embedded in config.Config and queried via f.Get[...]()
f.StringP("repo", "r", "", "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringP("password-file", "p", "", "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory. (default: use system default cache directory)")
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory")
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
f.StringSliceVar(&globalOptions.CACerts, "cacert", nil, "`file` to load root certificates from (default: use system certificates)")
f.StringVar(&globalOptions.TLSClientCert, "tls-client-cert", "", "path to a file containing PEM encoded TLS client certificate and private key")
@@ -186,6 +180,7 @@ func Printf(format string, args ...interface{}) {
_, err := fmt.Fprintf(globalOptions.stdout, format, args...)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
Exit(100)
}
}
@@ -226,6 +221,7 @@ func Warnf(format string, args ...interface{}) {
_, err := fmt.Fprintf(globalOptions.stderr, format, args...)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err)
Exit(100)
}
}
@@ -242,22 +238,10 @@ func Exitf(exitcode int, format string, args ...interface{}) {
// resolvePassword determines the password to be used for opening the repository.
func resolvePassword(opts GlobalOptions) (string, error) {
if opts.PasswordFile != "" && opts.PasswordCommand != "" {
return "", errors.Fatalf("Password file and command are mutually exclusive options")
}
if opts.PasswordCommand != "" {
args, err := backend.SplitShellStrings(opts.PasswordCommand)
if err != nil {
return "", err
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = os.Stderr
output, err := cmd.Output()
if err != nil {
return "", err
}
return (strings.TrimSpace(string(output))), nil
if opts.Password != "" {
return opts.Password, nil
}
if opts.PasswordFile != "" {
s, err := textfile.Read(opts.PasswordFile)
if os.IsNotExist(errors.Cause(err)) {
@@ -266,19 +250,20 @@ func resolvePassword(opts GlobalOptions) (string, error) {
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
}
if pwd := os.Getenv("RESTIC_PASSWORD"); pwd != "" {
return pwd, nil
}
return "", nil
}
// readPassword reads the password from the given reader directly.
func readPassword(in io.Reader) (password string, err error) {
sc := bufio.NewScanner(in)
sc.Scan()
buf := make([]byte, 1000)
n, err := io.ReadFull(in, buf)
buf = buf[:n]
return sc.Text(), errors.Wrap(err, "Scan")
if err != nil && errors.Cause(err) != io.ErrUnexpectedEOF {
return "", errors.Wrap(err, "ReadFull")
}
return strings.TrimRight(string(buf), "\r\n"), nil
}
// readPasswordTerminal reads the password from the given reader which must be a
@@ -312,7 +297,6 @@ func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
password, err = readPasswordTerminal(os.Stdin, os.Stderr, prompt)
} else {
password, err = readPassword(os.Stdin)
Verbosef("read password from stdin\n")
}
if err != nil {
@@ -320,7 +304,7 @@ func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
}
if len(password) == 0 {
return "", errors.New("an empty password is not a password")
return "", errors.Fatal("an empty password is not a password")
}
return password, nil
@@ -333,15 +317,13 @@ func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, er
if err != nil {
return "", err
}
if stdinIsTerminal() {
pw2, err := ReadPassword(gopts, prompt2)
if err != nil {
return "", err
}
pw2, err := ReadPassword(gopts, prompt2)
if err != nil {
return "", err
}
if pw1 != pw2 {
return "", errors.Fatal("passwords do not match")
}
if pw1 != pw2 {
return "", errors.Fatal("passwords do not match")
}
return pw1, nil
@@ -366,42 +348,22 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
s := repository.New(be)
passwordTriesLeft := 1
if stdinIsTerminal() && opts.password == "" {
passwordTriesLeft = 3
}
for ; passwordTriesLeft > 0; passwordTriesLeft-- {
opts.password, err = ReadPassword(opts, "enter password for repository: ")
if err != nil && passwordTriesLeft > 1 {
opts.password = ""
fmt.Printf("%s. Try again\n", err)
}
if err != nil {
continue
}
err = s.SearchKey(opts.ctx, opts.password, maxKeys, opts.KeyHint)
if err != nil && passwordTriesLeft > 1 {
opts.password = ""
fmt.Printf("%s. Try again\n", err)
}
}
opts.password, err = ReadPassword(opts, "enter password for repository: ")
if err != nil {
if errors.IsFatal(err) {
return nil, err
}
return nil, errors.Fatalf("%s", err)
return nil, err
}
if stdoutIsTerminal() && !opts.JSON {
err = s.SearchKey(opts.ctx, opts.password, maxKeys)
if err != nil {
return nil, err
}
if stdoutIsTerminal() {
id := s.Config().ID
if len(id) > 8 {
id = id[:8]
}
if !opts.JSON {
Verbosef("repository %v opened successfully, password is correct\n", id)
}
Verbosef("repository %v opened successfully, password is correct\n", id)
}
if opts.NoCache {
@@ -414,10 +376,6 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
return s, nil
}
if c.Created && !opts.JSON {
Verbosef("created new cache in %v\n", c.Base)
}
// start using the cache
s.UseCache(c)
@@ -444,7 +402,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
}
} else {
if stdoutIsTerminal() {
Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n",
Verbosef("found %d old cache directories in %v, pass --cleanup-cache to remove them\n",
len(oldCacheDirs), c.Base)
}
}
@@ -485,10 +443,6 @@ func parseConfig(loc location.Location, opts options.Options) (interface{}, erro
cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
if cfg.Region == "" {
cfg.Region = os.Getenv("AWS_DEFAULT_REGION")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}

View File

@@ -1,4 +1,3 @@
// +build !netbsd
// +build !openbsd
// +build !solaris
// +build !windows

View File

@@ -9,10 +9,11 @@ import (
"runtime"
"testing"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/config"
"github.com/restic/restic/internal/ui/options"
)
type dirEntry struct {
@@ -66,7 +67,7 @@ func isSymlink(fi os.FileInfo) bool {
func sameModTime(fi1, fi2 os.FileInfo) bool {
switch runtime.GOOS {
case "darwin", "freebsd", "openbsd", "netbsd":
case "darwin", "freebsd", "openbsd":
if isSymlink(fi1) && isSymlink(fi2) {
return true
}
@@ -209,7 +210,9 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
rtest.OK(t, os.MkdirAll(env.repo, 0700))
env.gopts = GlobalOptions{
Repo: env.repo,
Config: config.Config{
Repo: env.repo,
},
Quiet: true,
CacheDir: env.cache,
ctx: context.Background(),

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