mirror of
https://github.com/restic/restic.git
synced 2026-09-28 06:50:30 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abb1dc4eb6 | ||
|
|
8d21bb92db | ||
|
|
0b3c402801 | ||
|
|
b3b70002ab | ||
|
|
4916ba7a8a | ||
|
|
ea565df3e8 | ||
|
|
0758c92afc | ||
|
|
8b0092908a | ||
|
|
ffd7bc1021 | ||
|
|
6bad560324 | ||
|
|
7ad648c686 | ||
|
|
0c078cc205 | ||
|
|
1fbcf63830 | ||
|
|
740e2d6139 | ||
|
|
aaef54559a | ||
|
|
722517c480 |
+62
-10
@@ -1,9 +1,17 @@
|
|||||||
<!--
|
<!--
|
||||||
|
|
||||||
Welcome! If you have a question or are unsure if you should open an issue,
|
Welcome! - We kindly ask that you:
|
||||||
please use the forum instead!
|
|
||||||
|
|
||||||
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
|
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
|
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.
|
the software itself, rather than the project.
|
||||||
|
|
||||||
Thanks for understanding, and for contributing to 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
|
This section should include at least:
|
||||||
later to see what has changed in restic when we revisit this issue after some
|
|
||||||
time.
|
* 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/
|
||||||
|
-->
|
||||||
|
|||||||
@@ -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/
|
|
||||||
-->
|
|
||||||
@@ -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/
|
|
||||||
-->
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Thank you very much for contributing code or documentation to restic! Please
|
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
|
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.
|
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.
|
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.
|
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 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
|
- [ ] I have added tests for all changes in this PR
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
/restic
|
/restic
|
||||||
/.vagrant
|
/.vagrant
|
||||||
|
/doc/_build
|
||||||
|
|||||||
+7
-32
@@ -4,45 +4,17 @@ sudo: false
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: linux
|
- os: linux
|
||||||
go: "1.10.x"
|
go: "1.9.x"
|
||||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0 RESTIC_BUILD_SOLARIS=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
|
|
||||||
|
|
||||||
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
|
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
|
||||||
- os: linux
|
- os: linux
|
||||||
go: "1.13.x"
|
go: "1.10.x"
|
||||||
sudo: true
|
sudo: true
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- $HOME/.cache/go-build
|
|
||||||
- $HOME/gopath/pkg/mod
|
|
||||||
|
|
||||||
- os: osx
|
- os: osx
|
||||||
go: "1.13.x"
|
go: "1.10.x"
|
||||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- $HOME/Library/Caches/go-build
|
|
||||||
- $HOME/gopath/pkg/mod
|
|
||||||
|
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
@@ -64,3 +36,6 @@ install:
|
|||||||
|
|
||||||
script:
|
script:
|
||||||
- go run run_integration_tests.go
|
- go run run_integration_tests.go
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash) -f all.cov
|
||||||
|
|||||||
-666
@@ -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)
|
Changelog for restic 0.9.1 (2018-06-10)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
|||||||
+24
-53
@@ -46,15 +46,12 @@ Remember, the easier it is for us to reproduce the bug, the earlier it will be
|
|||||||
corrected!
|
corrected!
|
||||||
|
|
||||||
In addition, you can compile restic with debug support by running
|
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
|
`go run build.go -tags debug` and instructing it to create a debug log by
|
||||||
log by setting the environment variable `DEBUG_LOG` to a file, e.g. like this:
|
setting the environment variable `DEBUG_LOG` to a file, e.g. like this:
|
||||||
|
|
||||||
$ export DEBUG_LOG=/tmp/restic-debug.log
|
$ export DEBUG_LOG=/tmp/restic-debug.log
|
||||||
$ restic backup ~/work
|
$ 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
|
Please be aware that the debug log file will contain potentially sensitive
|
||||||
things like file and directory names, so please either redact it before
|
things like file and directory names, so please either redact it before
|
||||||
uploading it somewhere or post only the parts that are really relevant.
|
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
|
Development Environment
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
The repository contains several sets of directories with code: `cmd/` and
|
In order to compile restic with the `go` tool directly, it needs to be checked
|
||||||
`internal/` contain the code written for restic, whereas `vendor/` contains
|
out at the right path within a `GOPATH`. The concept of a `GOPATH` is explained
|
||||||
copies of libraries restic depends on. The libraries are managed with the
|
in ["How to write Go code"](https://golang.org/doc/code.html).
|
||||||
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).
|
|
||||||
|
|
||||||
If you do not have a directory with Go code yet, executing the following
|
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
|
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:
|
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
|
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
|
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 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,
|
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.
|
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
|
existing commit, use common sense to decide which is better), they will be
|
||||||
automatically added to the pull request.
|
automatically added to the pull request.
|
||||||
|
|
||||||
7. If your pull request changes anything that users should be aware
|
7. If your pull request changes anything that users should be aware of (a
|
||||||
of (a bugfix, a new feature, ...) please add an entry as a new
|
bugfix, a new feature, ...) please add an entry to the file
|
||||||
file in `changelog/unreleased` including the issue number in the
|
['CHANGELOG.md'](CHANGELOG.md). It will be used in the announcement of the
|
||||||
filename (e.g. `issue-8756`). Use the template in
|
next stable release. While writing, ask yourself: If I were the user, what
|
||||||
`changelog/TEMPLATE` for the content. It will be used in the
|
would I need to be aware of with this change.
|
||||||
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
|
8. Once your code looks good and passes all the tests, we'll merge it. Thanks
|
||||||
a lot for your contribution!
|
a lot for your contribution!
|
||||||
@@ -169,14 +141,13 @@ run
|
|||||||
|
|
||||||
gofmt -w **/*.go
|
gofmt -w **/*.go
|
||||||
|
|
||||||
in the project root directory before committing. For each Pull Request, the
|
in the project root directory before committing. Installing the script
|
||||||
formatting is tested with `gofmt` for the latest stable version of Go.
|
`fmt-check` from https://github.com/edsrzf/gofmt-git-hook locally as a
|
||||||
Installing the script `fmt-check` from https://github.com/edsrzf/gofmt-git-hook
|
pre-commit hook checks formatting before committing automatically, just copy
|
||||||
locally as a pre-commit hook checks formatting before committing automatically,
|
this script to `.git/hooks/pre-commit`.
|
||||||
just copy this script to `.git/hooks/pre-commit`.
|
|
||||||
|
|
||||||
For each pull request, several different systems run the integration tests on
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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`
|
make several smaller commits. This is much easier to review. Using `git add -p`
|
||||||
|
|||||||
Generated
+255
@@ -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
@@ -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"
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
all: restic
|
all: restic
|
||||||
|
|
||||||
restic:
|
restic:
|
||||||
go run -mod=vendor build.go || go run build.go
|
go run build.go
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f restic
|
rm -f restic
|
||||||
|
|||||||
+4
-14
@@ -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
|
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>`__.
|
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
|
Restic is licensed under `BSD 2-Clause License <https://opensource.org/licenses/BSD-2-Clause>`__. You can find the
|
||||||
complete text in ``LICENSE``.
|
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
|
.. |Documentation| image:: https://readthedocs.org/projects/restic/badge/?version=latest
|
||||||
:target: https://restic.readthedocs.io/en/latest/?badge=latest
|
:target: https://restic.readthedocs.io/en/latest/?badge=latest
|
||||||
.. |Build Status| image:: https://travis-ci.com/restic/restic.svg?branch=master
|
.. |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
|
:target: https://goreportcard.com/report/github.com/restic/restic
|
||||||
.. |Say Thanks| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
|
.. |Say Thanks| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
|
||||||
:target: https://saythanks.io/to/restic
|
:target: https://saythanks.io/to/restic
|
||||||
.. |AppsCode| image:: https://cdn.appscode.com/images/logo/appscode/ac-logo-color.png
|
.. |TestCoverage| image:: https://codecov.io/gh/restic/restic/branch/master/graph/badge.svg
|
||||||
:target: https://appscode.com
|
:target: https://codecov.io/gh/restic/restic
|
||||||
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
|
|
||||||
:target: https://houndci.com
|
|
||||||
|
|||||||
+3
-6
@@ -7,9 +7,6 @@ branches:
|
|||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
cache:
|
|
||||||
- '%LocalAppData%\go-build'
|
|
||||||
|
|
||||||
init:
|
init:
|
||||||
- ps: >-
|
- ps: >-
|
||||||
$app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'"
|
$app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'"
|
||||||
@@ -20,8 +17,8 @@ init:
|
|||||||
|
|
||||||
install:
|
install:
|
||||||
- rmdir c:\go /s /q
|
- rmdir c:\go /s /q
|
||||||
- appveyor DownloadFile https://dl.google.com/go/go1.13.4.windows-amd64.msi
|
- appveyor DownloadFile https://dl.google.com/go/go1.10.windows-amd64.msi
|
||||||
- msiexec /i go1.13.4.windows-amd64.msi /q
|
- msiexec /i go1.10.windows-amd64.msi /q
|
||||||
- go version
|
- go version
|
||||||
- go env
|
- go env
|
||||||
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip
|
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip
|
||||||
@@ -29,4 +26,4 @@ install:
|
|||||||
- set PATH=bin/;%PATH%
|
- set PATH=bin/;%PATH%
|
||||||
|
|
||||||
build_script:
|
build_script:
|
||||||
- go run -mod=vendor run_integration_tests.go
|
- go run run_integration_tests.go
|
||||||
|
|||||||
@@ -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
|
// BSD 2-Clause License
|
||||||
//
|
//
|
||||||
// Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
|
// Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
|
||||||
@@ -52,6 +37,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -60,22 +46,23 @@ import (
|
|||||||
|
|
||||||
// config contains the configuration for the program to build.
|
// config contains the configuration for the program to build.
|
||||||
var config = Config{
|
var config = Config{
|
||||||
Name: "restic", // name of the program executable and directory
|
Name: "restic", // name of the program executable and directory
|
||||||
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
||||||
Main: "./cmd/restic", // package name for the main package
|
Main: "github.com/restic/restic/cmd/restic", // package name for the main package
|
||||||
DefaultBuildTags: []string{"selfupdate"}, // specify build tags which are always used
|
Tests: []string{ // tests to run
|
||||||
Tests: []string{"./..."}, // tests to run
|
"github.com/restic/restic/internal/...",
|
||||||
MinVersion: GoVersion{Major: 1, Minor: 10, Patch: 0}, // minimum Go version supported
|
"github.com/restic/restic/cmd/...",
|
||||||
|
},
|
||||||
|
MinVersion: GoVersion{Major: 1, Minor: 9, Patch: 0}, // minimum Go version supported
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config configures the build.
|
// Config configures the build.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Name string
|
Name string
|
||||||
Namespace string
|
Namespace string
|
||||||
Main string
|
Main string
|
||||||
DefaultBuildTags []string
|
Tests []string
|
||||||
Tests []string
|
MinVersion GoVersion
|
||||||
MinVersion GoVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -84,12 +71,41 @@ var (
|
|||||||
runTests bool
|
runTests bool
|
||||||
enableCGO bool
|
enableCGO bool
|
||||||
enablePIE 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
|
// with "/home/u/restic" containing the file "foo.go" yields the following tree
|
||||||
// at "/tmp/gopath":
|
// at "/tmp/gopath":
|
||||||
@@ -100,15 +116,19 @@ var (
|
|||||||
// └── restic
|
// └── restic
|
||||||
// └── restic
|
// └── restic
|
||||||
// └── foo.go
|
// └── foo.go
|
||||||
func copy(dst, src string) error {
|
func updateGopath(dst, src, prefix string) error {
|
||||||
verbosePrintf("copy contents of %v to %v\n", src, dst)
|
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 {
|
return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
|
||||||
if name == src {
|
if name == src {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if name == ".git" {
|
if specialDir(name) {
|
||||||
return filepath.SkipDir
|
if fi.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -119,13 +139,17 @@ func copy(dst, src string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if excludePath(name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
intermediatePath, err := filepath.Rel(src, name)
|
intermediatePath, err := filepath.Rel(src, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileSrc := filepath.Join(src, intermediatePath)
|
fileSrc := filepath.Join(src, intermediatePath)
|
||||||
fileDst := filepath.Join(dst, intermediatePath)
|
fileDst := filepath.Join(dst, "src", prefix, intermediatePath)
|
||||||
|
|
||||||
return copyFile(fileDst, fileSrc)
|
return copyFile(fileDst, fileSrc)
|
||||||
})
|
})
|
||||||
@@ -140,15 +164,6 @@ func directoryExists(dirname string) bool {
|
|||||||
return stat.IsDir()
|
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.
|
// copyFile creates dst from src, preserving file attributes and timestamps.
|
||||||
func copyFile(dst, src string) error {
|
func copyFile(dst, src string) error {
|
||||||
fi, err := os.Stat(src)
|
fi, err := os.Stat(src)
|
||||||
@@ -168,34 +183,30 @@ func copyFile(dst, src string) error {
|
|||||||
|
|
||||||
fdst, err := os.Create(dst)
|
fdst, err := os.Create(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = fsrc.Close()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(fdst, fsrc)
|
if _, err = io.Copy(fdst, fsrc); err != nil {
|
||||||
if err != nil {
|
|
||||||
_ = fsrc.Close()
|
|
||||||
_ = fdst.Close()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = fdst.Close()
|
if err == nil {
|
||||||
if err != nil {
|
err = fsrc.Close()
|
||||||
_ = fsrc.Close()
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = fsrc.Close()
|
if err == nil {
|
||||||
if err != nil {
|
err = fdst.Close()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Chmod(dst, fi.Mode())
|
if err == nil {
|
||||||
if err != nil {
|
err = os.Chmod(dst, fi.Mode())
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// 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, "OPTIONS:\n")
|
||||||
fmt.Fprintf(output, " -v --verbose output more messages\n")
|
fmt.Fprintf(output, " -v --verbose output more messages\n")
|
||||||
fmt.Fprintf(output, " -t --tags specify additional build tags\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, " -T --test run tests\n")
|
||||||
fmt.Fprintf(output, " -o --output set output file name\n")
|
fmt.Fprintf(output, " -o --output set output file name\n")
|
||||||
fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\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...)
|
fmt.Printf("build: "+message, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanEnv returns a clean environment with GOPATH, GOBIN and GO111MODULE
|
// cleanEnv returns a clean environment with GOPATH and GOBIN removed (if
|
||||||
// removed (if present).
|
// present).
|
||||||
func cleanEnv() (env []string) {
|
func cleanEnv() (env []string) {
|
||||||
removeKeys := map[string]struct{}{
|
|
||||||
"GOPATH": struct{}{},
|
|
||||||
"GOBIN": struct{}{},
|
|
||||||
"GO111MODULE": struct{}{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range os.Environ() {
|
for _, v := range os.Environ() {
|
||||||
data := strings.SplitN(v, "=", 2)
|
if strings.HasPrefix(v, "GOPATH=") || strings.HasPrefix(v, "GOBIN=") {
|
||||||
name := data[0]
|
|
||||||
|
|
||||||
if _, ok := removeKeys[name]; ok {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,17 +256,17 @@ func cleanEnv() (env []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// build runs "go build args..." with GOPATH set to gopath.
|
// 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"}
|
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")
|
verbosePrintf("Go version is at least 1.10, using new syntax for -gcflags\n")
|
||||||
// use new prefix
|
// use new prefix
|
||||||
a = append(a, "-asmflags", 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", cwd))
|
a = append(a, "-gcflags", fmt.Sprintf("all=-trimpath=%s", gopath))
|
||||||
} else {
|
} else {
|
||||||
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", cwd))
|
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
|
||||||
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", cwd))
|
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
|
||||||
}
|
}
|
||||||
if enablePIE {
|
if enablePIE {
|
||||||
a = append(a, "-buildmode=pie")
|
a = append(a, "-buildmode=pie")
|
||||||
@@ -272,9 +274,9 @@ func build(cwd string, env map[string]string, args ...string) error {
|
|||||||
|
|
||||||
a = append(a, args...)
|
a = append(a, args...)
|
||||||
cmd := exec.Command("go", a...)
|
cmd := exec.Command("go", a...)
|
||||||
cmd.Env = append(cleanEnv(), "GOPROXY=off")
|
cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos)
|
||||||
for k, v := range env {
|
if goarm != "" {
|
||||||
cmd.Env = append(cmd.Env, k+"="+v)
|
cmd.Env = append(cmd.Env, "GOARM="+goarm)
|
||||||
}
|
}
|
||||||
if !enableCGO {
|
if !enableCGO {
|
||||||
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
|
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.Dir = cwd
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
verbosePrintf("go %s\n", a)
|
||||||
verbosePrintf("chdir %q\n", cwd)
|
|
||||||
verbosePrintf("go %q\n", a)
|
|
||||||
|
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// test runs "go test args..." with GOPATH set to gopath.
|
// test runs "go test args..." with GOPATH set to gopath.
|
||||||
func test(cwd string, env map[string]string, args ...string) error {
|
func test(cwd, gopath string, args ...string) error {
|
||||||
args = append([]string{"test", "-count", "1"}, args...)
|
args = append([]string{"test"}, args...)
|
||||||
cmd := exec.Command("go", args...)
|
cmd := exec.Command("go", args...)
|
||||||
cmd.Env = append(cleanEnv(), "GOPROXY=off")
|
cmd.Env = append(cleanEnv(), "GOPATH="+gopath)
|
||||||
for k, v := range env {
|
|
||||||
cmd.Env = append(cmd.Env, k+"="+v)
|
|
||||||
}
|
|
||||||
if !enableCGO {
|
|
||||||
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
|
|
||||||
}
|
|
||||||
cmd.Dir = cwd
|
cmd.Dir = cwd
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
verbosePrintf("go %s\n", args)
|
||||||
verbosePrintf("chdir %q\n", cwd)
|
|
||||||
verbosePrintf("go %q\n", args)
|
|
||||||
|
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
@@ -454,24 +446,22 @@ func (v GoVersion) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if !goVersion.AtLeast(config.MinVersion) {
|
ver := ParseGoVersion(runtime.Version())
|
||||||
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion)
|
if !ver.AtLeast(config.MinVersion) {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", ver, config.MinVersion)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTags := config.DefaultBuildTags
|
buildTags := []string{}
|
||||||
|
|
||||||
skipNext := false
|
skipNext := false
|
||||||
params := os.Args[1:]
|
params := os.Args[1:]
|
||||||
|
|
||||||
goEnv := map[string]string{}
|
targetGOOS := runtime.GOOS
|
||||||
buildEnv := map[string]string{
|
targetGOARCH := runtime.GOARCH
|
||||||
"GOOS": runtime.GOOS,
|
targetGOARM := ""
|
||||||
"GOARCH": runtime.GOARCH,
|
|
||||||
"GOARM": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
tempdir := ""
|
gopath := ""
|
||||||
|
|
||||||
var outputFilename string
|
var outputFilename string
|
||||||
|
|
||||||
@@ -491,13 +481,13 @@ func main() {
|
|||||||
die("-t given but no tag specified")
|
die("-t given but no tag specified")
|
||||||
}
|
}
|
||||||
skipNext = true
|
skipNext = true
|
||||||
buildTags = append(buildTags, strings.Split(params[i+1], " ")...)
|
buildTags = strings.Split(params[i+1], " ")
|
||||||
case "-o", "--output":
|
case "-o", "--output":
|
||||||
skipNext = true
|
skipNext = true
|
||||||
outputFilename = params[i+1]
|
outputFilename = params[i+1]
|
||||||
case "--tempdir":
|
case "--tempdir":
|
||||||
skipNext = true
|
skipNext = true
|
||||||
tempdir = params[i+1]
|
gopath = params[i+1]
|
||||||
case "-T", "--test":
|
case "-T", "--test":
|
||||||
runTests = true
|
runTests = true
|
||||||
case "--enable-cgo":
|
case "--enable-cgo":
|
||||||
@@ -506,13 +496,13 @@ func main() {
|
|||||||
enablePIE = true
|
enablePIE = true
|
||||||
case "--goos":
|
case "--goos":
|
||||||
skipNext = true
|
skipNext = true
|
||||||
buildEnv["GOOS"] = params[i+1]
|
targetGOOS = params[i+1]
|
||||||
case "--goarch":
|
case "--goarch":
|
||||||
skipNext = true
|
skipNext = true
|
||||||
buildEnv["GOARCH"] = params[i+1]
|
targetGOARCH = params[i+1]
|
||||||
case "--goarm":
|
case "--goarm":
|
||||||
skipNext = true
|
skipNext = true
|
||||||
buildEnv["GOARM"] = params[i+1]
|
targetGOARM = params[i+1]
|
||||||
case "-h":
|
case "-h":
|
||||||
showUsage(os.Stdout)
|
showUsage(os.Stdout)
|
||||||
return
|
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 {
|
for i := range buildTags {
|
||||||
buildTags[i] = strings.TrimSpace(buildTags[i])
|
buildTags[i] = strings.TrimSpace(buildTags[i])
|
||||||
@@ -536,16 +531,50 @@ func main() {
|
|||||||
die("Getwd(): %v\n", err)
|
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 == "" {
|
if outputFilename == "" {
|
||||||
outputFilename = config.Name
|
outputFilename = config.Name
|
||||||
if buildEnv["GOOS"] == "windows" {
|
if targetGOOS == "windows" {
|
||||||
outputFilename += ".exe"
|
outputFilename += ".exe"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
die("Getwd() returned %v\n", err)
|
||||||
|
}
|
||||||
output := outputFilename
|
output := outputFilename
|
||||||
if !filepath.IsAbs(output) {
|
if !filepath.IsAbs(output) {
|
||||||
output = filepath.Join(root, output)
|
output = filepath.Join(cwd, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
version := getVersion()
|
version := getVersion()
|
||||||
@@ -556,68 +585,13 @@ func main() {
|
|||||||
ldflags := "-s -w " + constants.LDFlags()
|
ldflags := "-s -w " + constants.LDFlags()
|
||||||
verbosePrintf("ldflags: %s\n", ldflags)
|
verbosePrintf("ldflags: %s\n", ldflags)
|
||||||
|
|
||||||
var (
|
args := []string{
|
||||||
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,
|
|
||||||
"-tags", strings.Join(buildTags, " "),
|
"-tags", strings.Join(buildTags, " "),
|
||||||
"-ldflags", ldflags,
|
"-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 {
|
if err != nil {
|
||||||
die("build failed: %v\n", err)
|
die("build failed: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -625,9 +599,7 @@ func main() {
|
|||||||
if runTests {
|
if runTests {
|
||||||
verbosePrintf("running tests\n")
|
verbosePrintf("running tests\n")
|
||||||
|
|
||||||
testArgs = append(testArgs, config.Tests...)
|
err = test(cwd, gopath, config.Tests...)
|
||||||
|
|
||||||
err = test(buildCWD, goEnv, testArgs...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
die("running tests failed: %v\n", err)
|
die("running tests failed: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
Bugfix: Don't cancel timeout after 30 seconds for self-update
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2181
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+91
-205
@@ -4,12 +4,8 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,7 +21,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/textfile"
|
"github.com/restic/restic/internal/textfile"
|
||||||
"github.com/restic/restic/internal/ui"
|
"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"
|
"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.
|
given as the arguments.
|
||||||
`,
|
`,
|
||||||
PreRun: func(cmd *cobra.Command, args []string) {
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
if backupOptions.Host == "" {
|
if backupOptions.Hostname == "" {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.Log("os.Hostname() returned err: %v", err)
|
debug.Log("os.Hostname() returned err: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupOptions.Host = hostname
|
backupOptions.Hostname = hostname
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if backupOptions.Stdin {
|
err := config.ApplyFlags(&backupOptions.Config, cmd.Flags())
|
||||||
for _, filename := range backupOptions.FilesFrom {
|
if err != nil {
|
||||||
if filename == "-" {
|
return err
|
||||||
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
|
}
|
||||||
}
|
|
||||||
}
|
if backupOptions.Stdin && backupOptions.FilesFrom == "-" {
|
||||||
|
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
|
||||||
}
|
}
|
||||||
|
|
||||||
var t tomb.Tomb
|
var t tomb.Tomb
|
||||||
term := termstatus.New(globalOptions.stdout, globalOptions.stderr, globalOptions.Quiet)
|
term := termstatus.New(globalOptions.stdout, globalOptions.stderr, globalOptions.Quiet)
|
||||||
t.Go(func() error { term.Run(t.Context(globalOptions.ctx)); return nil })
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -71,22 +68,21 @@ given as the arguments.
|
|||||||
|
|
||||||
// BackupOptions bundles all options for the backup command.
|
// BackupOptions bundles all options for the backup command.
|
||||||
type BackupOptions struct {
|
type BackupOptions struct {
|
||||||
Parent string
|
Config config.Backup
|
||||||
Force bool
|
|
||||||
Excludes []string
|
Parent string
|
||||||
InsensitiveExcludes []string
|
Force bool
|
||||||
ExcludeFiles []string
|
ExcludeFiles []string
|
||||||
ExcludeOtherFS bool
|
ExcludeOtherFS bool
|
||||||
ExcludeIfPresent []string
|
ExcludeIfPresent []string
|
||||||
ExcludeCaches bool
|
ExcludeCaches bool
|
||||||
Stdin bool
|
Stdin bool
|
||||||
StdinFilename string
|
StdinFilename string
|
||||||
Tags []string
|
Tags []string
|
||||||
Host string
|
Hostname string
|
||||||
FilesFrom []string
|
FilesFrom string
|
||||||
TimeStamp string
|
TimeStamp string
|
||||||
WithAtime bool
|
WithAtime bool
|
||||||
IgnoreInode bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupOptions BackupOptions
|
var backupOptions BackupOptions
|
||||||
@@ -97,24 +93,20 @@ func init() {
|
|||||||
f := cmdBackup.Flags()
|
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.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.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.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.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
|
||||||
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
||||||
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See 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.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.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.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (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.StringVarP(&backupOptions.Host, "host", "H", "", "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.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.TimeStamp, "time", "", "time of the backup (ex. '2012-11-01 22:08:41') (default: now)")
|
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.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
|
// 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.
|
// Check returns an error when an invalid combination of options was set.
|
||||||
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
||||||
if gopts.password == "" {
|
if opts.FilesFrom == "-" && gopts.password == "" {
|
||||||
for _, filename := range opts.FilesFrom {
|
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
|
||||||
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.Stdin {
|
if opts.Stdin {
|
||||||
if len(opts.FilesFrom) > 0 {
|
if opts.FilesFrom != "" {
|
||||||
return errors.Fatal("--stdin and --files-from cannot be used together")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
// collectRejectFuncs returns a list of all functions which may reject data
|
||||||
// from being saved in a snapshot based on path only
|
// from being saved in a snapshot
|
||||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectByNameFunc, err error) {
|
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
|
// exclude restic cache
|
||||||
if repo.Cache != nil {
|
if repo.Cache != nil {
|
||||||
f, err := rejectResticCache(repo)
|
f, err := rejectResticCache(repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fs = append(fs, f)
|
fs = append(fs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
excludes = append(excludes, opts.Config.Excludes...)
|
||||||
|
|
||||||
// add patterns from file
|
// add patterns from file
|
||||||
if len(opts.ExcludeFiles) > 0 {
|
if len(opts.ExcludeFiles) > 0 {
|
||||||
excludes, err := readExcludePatternsFromFiles(opts.ExcludeFiles)
|
excludes = append(excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
opts.Excludes = append(opts.Excludes, excludes...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opts.InsensitiveExcludes) > 0 {
|
if len(excludes) > 0 {
|
||||||
fs = append(fs, rejectByInsensitivePattern(opts.InsensitiveExcludes))
|
fs = append(fs, rejectByPattern(excludes))
|
||||||
}
|
|
||||||
|
|
||||||
if len(opts.Excludes) > 0 {
|
|
||||||
fs = append(fs, rejectByPattern(opts.Excludes))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ExcludeCaches {
|
if opts.ExcludeCaches {
|
||||||
@@ -244,43 +235,18 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, t
|
|||||||
for _, spec := range opts.ExcludeIfPresent {
|
for _, spec := range opts.ExcludeIfPresent {
|
||||||
f, err := rejectIfPresent(spec)
|
f, err := rejectIfPresent(spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fs = append(fs, f)
|
fs = append(fs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs, nil
|
return fs, excludes, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readExcludePatternsFromFiles reads all exclude files and returns the list of
|
// readExcludePatternsFromFiles reads all exclude files and returns the list of
|
||||||
// exclude patterns. For each line, leading and trailing white space is removed
|
// exclude patterns.
|
||||||
// and comment lines are ignored. For each remaining pattern, environment
|
func readExcludePatternsFromFiles(excludeFiles []string) []string {
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var excludes []string
|
var excludes []string
|
||||||
for _, filename := range excludeFiles {
|
for _, filename := range excludeFiles {
|
||||||
err := func() (err error) {
|
err := func() (err error) {
|
||||||
@@ -303,16 +269,17 @@ func readExcludePatternsFromFiles(excludeFiles []string) ([]string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
line = os.Expand(line, getenvOrDollar)
|
line = os.ExpandEnv(line)
|
||||||
excludes = append(excludes, line)
|
excludes = append(excludes, line)
|
||||||
}
|
}
|
||||||
return scanner.Err()
|
return scanner.Err()
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
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.
|
// 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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var lines []string
|
fromfile, err := readLinesFromFile(opts.FilesFrom)
|
||||||
for _, file := range opts.FilesFrom {
|
if err != nil {
|
||||||
fromfile, err := readLinesFromFile(file)
|
return nil, err
|
||||||
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...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge files from files-from into normal args so we can reuse the normal
|
// 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
|
// args checks and have the ability to use both files-from and args at the
|
||||||
// same time
|
// same time
|
||||||
args = append(args, lines...)
|
args = append(args, fromfile...)
|
||||||
if len(args) == 0 && !opts.Stdin {
|
if len(args) == 0 && !opts.Stdin {
|
||||||
return nil, errors.Fatal("nothing to backup, please specify target files/dirs")
|
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
|
// Find last snapshot to set it as parent, if not already set
|
||||||
if !opts.Force && parentID == nil {
|
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 {
|
if err == nil {
|
||||||
parentID = &id
|
parentID = &id
|
||||||
} else if err != restic.ErrNoSnapshotFound {
|
} else if err != restic.ErrNoSnapshotFound {
|
||||||
@@ -398,7 +349,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
|||||||
|
|
||||||
timeStamp := time.Now()
|
timeStamp := time.Now()
|
||||||
if opts.TimeStamp != "" {
|
if opts.TimeStamp != "" {
|
||||||
timeStamp, err = time.ParseInLocation(TimeFormat, opts.TimeStamp, time.Local)
|
timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("error in time option: %v\n", err)
|
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
|
var t tomb.Tomb
|
||||||
|
|
||||||
if gopts.verbosity >= 2 && !gopts.JSON {
|
p := ui.NewBackup(term, gopts.verbosity)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use the terminal for stdout/stderr
|
// use the terminal for stdout/stderr
|
||||||
prevStdout, prevStderr := gopts.stdout, gopts.stderr
|
prevStdout, prevStderr := gopts.stdout, gopts.stderr
|
||||||
@@ -457,36 +372,32 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
|||||||
if fps > 60 {
|
if fps > 60 {
|
||||||
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)) })
|
t.Go(func() error { return p.Run(t.Context(gopts.ctx)) })
|
||||||
|
|
||||||
if !gopts.JSON {
|
p.V("open repository")
|
||||||
p.V("lock repository")
|
repo, err := OpenRepository(gopts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.V("lock repository")
|
||||||
lock, err := lockRepo(repo)
|
lock, err := lockRepo(repo)
|
||||||
defer unlockRepo(lock)
|
defer unlockRepo(lock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
|
// rejectFuncs collect functions that can reject items from the backup
|
||||||
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, targets)
|
rejectFuncs, excludes, err := collectRejectFuncs(opts, repo, targets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
p.V("load index files")
|
||||||
rejectFuncs, err := collectRejectFuncs(opts, repo, targets)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !gopts.JSON {
|
|
||||||
p.V("load index files")
|
|
||||||
}
|
|
||||||
err = repo.LoadIndex(gopts.ctx)
|
err = repo.LoadIndex(gopts.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -497,19 +408,10 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gopts.JSON && parentSnapshotID != nil {
|
if parentSnapshotID != nil {
|
||||||
p.V("using parent snapshot %v\n", parentSnapshotID.Str())
|
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 {
|
selectFilter := func(item string, fi os.FileInfo) bool {
|
||||||
for _, reject := range rejectFuncs {
|
for _, reject := range rejectFuncs {
|
||||||
if reject(item, fi) {
|
if reject(item, fi) {
|
||||||
@@ -521,63 +423,51 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
|||||||
|
|
||||||
var targetFS fs.FS = fs.Local{}
|
var targetFS fs.FS = fs.Local{}
|
||||||
if opts.Stdin {
|
if opts.Stdin {
|
||||||
if !gopts.JSON {
|
p.V("read data from stdin")
|
||||||
p.V("read data from stdin")
|
|
||||||
}
|
|
||||||
filename := path.Join("/", opts.StdinFilename)
|
|
||||||
targetFS = &fs.Reader{
|
targetFS = &fs.Reader{
|
||||||
ModTime: timeStamp,
|
ModTime: timeStamp,
|
||||||
Name: filename,
|
Name: opts.StdinFilename,
|
||||||
Mode: 0644,
|
Mode: 0644,
|
||||||
ReadCloser: os.Stdin,
|
ReadCloser: os.Stdin,
|
||||||
}
|
}
|
||||||
targets = []string{filename}
|
targets = []string{opts.StdinFilename}
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := archiver.NewScanner(targetFS)
|
sc := archiver.NewScanner(targetFS)
|
||||||
sc.SelectByName = selectByNameFilter
|
|
||||||
sc.Select = selectFilter
|
sc.Select = selectFilter
|
||||||
sc.Error = p.ScannerError
|
sc.Error = p.ScannerError
|
||||||
sc.Result = p.ReportTotal
|
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) })
|
t.Go(func() error { return sc.Scan(t.Context(gopts.ctx), targets) })
|
||||||
|
|
||||||
arch := archiver.New(repo, targetFS, archiver.Options{})
|
arch := archiver.New(repo, targetFS, archiver.Options{})
|
||||||
arch.SelectByName = selectByNameFilter
|
|
||||||
arch.Select = selectFilter
|
arch.Select = selectFilter
|
||||||
arch.WithAtime = opts.WithAtime
|
arch.WithAtime = opts.WithAtime
|
||||||
arch.Error = p.Error
|
arch.Error = p.Error
|
||||||
arch.CompleteItem = p.CompleteItem
|
arch.CompleteItem = p.CompleteItemFn
|
||||||
arch.StartFile = p.StartFile
|
arch.StartFile = p.StartFile
|
||||||
arch.CompleteBlob = p.CompleteBlob
|
arch.CompleteBlob = p.CompleteBlob
|
||||||
arch.IgnoreInode = opts.IgnoreInode
|
|
||||||
|
|
||||||
if parentSnapshotID == nil {
|
if parentSnapshotID == nil {
|
||||||
parentSnapshotID = &restic.ID{}
|
parentSnapshotID = &restic.ID{}
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotOpts := archiver.SnapshotOptions{
|
snapshotOpts := archiver.SnapshotOptions{
|
||||||
Excludes: opts.Excludes,
|
Excludes: excludes,
|
||||||
Tags: opts.Tags,
|
Tags: opts.Tags,
|
||||||
Time: timeStamp,
|
Time: timeStamp,
|
||||||
Hostname: opts.Host,
|
Hostname: opts.Hostname,
|
||||||
ParentSnapshot: *parentSnapshotID,
|
ParentSnapshot: *parentSnapshotID,
|
||||||
}
|
}
|
||||||
|
|
||||||
uploader := archiver.IndexUploader{
|
uploader := archiver.IndexUploader{
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
Start: func() {
|
Start: func() {
|
||||||
if !gopts.JSON {
|
p.VV("uploading intermediate index")
|
||||||
p.VV("uploading intermediate index")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Complete: func(id restic.ID) {
|
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)
|
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)
|
_, id, err := arch.Snapshot(gopts.ctx, targets, snapshotOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Finish(id)
|
p.Finish()
|
||||||
if !gopts.JSON {
|
p.P("snapshot %s saved\n", id.Str())
|
||||||
p.P("snapshot %s saved\n", id.Str())
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanly shutdown all running goroutines
|
// cleanly shutdown all running goroutines
|
||||||
t.Kill(nil)
|
t.Kill(nil)
|
||||||
|
|||||||
+4
-48
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,7 +9,6 @@ import (
|
|||||||
"github.com/restic/restic/internal/cache"
|
"github.com/restic/restic/internal/cache"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +28,6 @@ The "cache" command allows listing and cleaning local cache directories.
|
|||||||
type CacheOptions struct {
|
type CacheOptions struct {
|
||||||
Cleanup bool
|
Cleanup bool
|
||||||
MaxAge uint
|
MaxAge uint
|
||||||
NoSize bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheOptions CacheOptions
|
var cacheOptions CacheOptions
|
||||||
@@ -41,7 +38,6 @@ func init() {
|
|||||||
f := cmdCache.Flags()
|
f := cmdCache.Flags()
|
||||||
f.BoolVar(&cacheOptions.Cleanup, "cleanup", false, "remove old cache directories")
|
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.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 {
|
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
||||||
@@ -89,22 +85,9 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tab := table.New()
|
tab := NewTable()
|
||||||
|
tab.Header = fmt.Sprintf("%-14s %-16s %s", "Repository ID", "Last Used", "Old")
|
||||||
type data struct {
|
tab.RowFormat = "%-14s %-16s %s"
|
||||||
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 }}")
|
|
||||||
}
|
|
||||||
|
|
||||||
dirs, err := cache.All(cachedir)
|
dirs, err := cache.All(cachedir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -126,41 +109,14 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
|||||||
old = "yes"
|
old = "yes"
|
||||||
}
|
}
|
||||||
|
|
||||||
var size string
|
tab.Rows = append(tab.Rows, []interface{}{
|
||||||
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{
|
|
||||||
entry.Name()[:10],
|
entry.Name()[:10],
|
||||||
fmt.Sprintf("%d days ago", uint(time.Since(entry.ModTime()).Hours()/24)),
|
fmt.Sprintf("%d days ago", uint(time.Since(entry.ModTime()).Hours()/24)),
|
||||||
old,
|
old,
|
||||||
size,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tab.Write(gopts.stdout)
|
tab.Write(gopts.stdout)
|
||||||
Printf("%d cache dirs in %s\n", len(dirs), cachedir)
|
|
||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
fmt.Println(string(buf))
|
fmt.Println(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "index":
|
case "index":
|
||||||
buf, err := repo.LoadAndDecrypt(gopts.ctx, nil, restic.IndexFile, id)
|
buf, err := repo.LoadAndDecrypt(gopts.ctx, restic.IndexFile, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
case "key":
|
case "key":
|
||||||
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
|
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
|
||||||
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
|
buf, err := backend.LoadAll(gopts.ctx, repo.Backend(), h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
|||||||
switch tpe {
|
switch tpe {
|
||||||
case "pack":
|
case "pack":
|
||||||
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
|
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
|
||||||
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
|
buf, err := backend.LoadAll(gopts.ctx, repo.Backend(), h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-13
@@ -50,7 +50,7 @@ func init() {
|
|||||||
|
|
||||||
f := cmdCheck.Flags()
|
f := cmdCheck.Flags()
|
||||||
f.BoolVar(&checkOptions.ReadData, "read-data", false, "read all data blobs")
|
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.CheckUnused, "check-unused", false, "find unused blobs")
|
||||||
f.BoolVar(&checkOptions.WithCache, "with-cache", false, "use the cache")
|
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] {
|
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")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// See doReadData in runCheck below for why this is 256.
|
|
||||||
const totalBucketsMax = 256
|
|
||||||
|
|
||||||
// stringToIntSlice converts string to []uint, using '/' as element separator
|
// stringToIntSlice converts string to []uint, using '/' as element separator
|
||||||
func stringToIntSlice(param string) (split []uint, err error) {
|
func stringToIntSlice(param string) (split []uint, err error) {
|
||||||
if param == "" {
|
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 --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 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
|
// * by default, we use a cache in a temporary directory that is deleted after the check
|
||||||
func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func()) {
|
func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func()) {
|
||||||
cleanup = func() {}
|
cleanup = func() {}
|
||||||
@@ -143,10 +136,8 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func())
|
|||||||
return cleanup
|
return cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedir := gopts.CacheDir
|
|
||||||
|
|
||||||
// use a cache in a temporary directory
|
// 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 err != nil {
|
||||||
// if an error occurs, don't use any cache
|
// if an error occurs, don't use any cache
|
||||||
Warnf("unable to create temporary directory for cache during check, disabling cache: %v\n", err)
|
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) {
|
doReadData := func(bucket, totalBuckets uint) {
|
||||||
packs := restic.IDSet{}
|
packs := restic.IDSet{}
|
||||||
for pack := range chkr.GetPacks() {
|
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) {
|
if (uint(pack[0]) % totalBuckets) == (bucket - 1) {
|
||||||
packs.Insert(pack)
|
packs.Insert(pack)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
first characters in each line display what has happened to a particular file or
|
||||||
directory:
|
directory:
|
||||||
|
|
||||||
* + The item was added
|
+ The item was added
|
||||||
* - The item was removed
|
- The item was removed
|
||||||
* U The metadata (access mode, timestamps, ...) for the item was updated
|
U The metadata (access mode, timestamps, ...) for the item was updated
|
||||||
* M The file's content was modified
|
M The file's content was modified
|
||||||
* T The type was changed, e.g. a file was made a symlink
|
T The type was changed, e.g. a file was made a symlink
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|||||||
+37
-155
@@ -1,19 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/walker"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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\"")
|
flags.StringArrayVar(&dumpOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitPath(p string) []string {
|
func splitPath(path string) []string {
|
||||||
d, f := path.Split(p)
|
d, f := filepath.Split(path)
|
||||||
if d == "" {
|
if d == "" || d == "/" {
|
||||||
return []string{f}
|
return []string{f}
|
||||||
}
|
}
|
||||||
if d == "/" {
|
s := splitPath(filepath.Clean(d))
|
||||||
return []string{d}
|
|
||||||
}
|
|
||||||
s := splitPath(path.Clean(d))
|
|
||||||
return append(s, f)
|
return append(s, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string, 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 {
|
if tree == nil {
|
||||||
return fmt.Errorf("called with a nil tree")
|
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])
|
item := filepath.Join(prefix, pathComponents[0])
|
||||||
for _, node := range tree.Nodes {
|
for _, node := range tree.Nodes {
|
||||||
if node.Name == pathComponents[0] || pathComponents[0] == "/" {
|
if node.Name == pathComponents[0] {
|
||||||
switch {
|
switch {
|
||||||
case l == 1 && node.Type == "file":
|
case l == 1 && node.Type == "file":
|
||||||
return getNodeData(ctx, os.Stdout, repo, node)
|
return dumpNode(ctx, repo, node)
|
||||||
case l > 1 && node.Type == "dir":
|
case l > 1 && node.Type == "dir":
|
||||||
subtree, err := repo.LoadTree(ctx, *node.Subtree)
|
subtree, err := repo.LoadTree(ctx, *node.Subtree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
||||||
}
|
}
|
||||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], pathToPrint)
|
return printFromTree(ctx, subtree, repo, item, pathComponents[1:])
|
||||||
case node.Type == "dir":
|
|
||||||
node.Path = pathToPrint
|
|
||||||
return tarTree(ctx, repo, node, pathToPrint)
|
|
||||||
case l > 1:
|
case l > 1:
|
||||||
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
return fmt.Errorf("%q should be a dir, but s a %q", item, node.Type)
|
||||||
case node.Type != "file":
|
case node.Type != "file":
|
||||||
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
||||||
}
|
}
|
||||||
@@ -113,7 +128,7 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
debug.Log("dump file %q from %q", pathToPrint, snapshotIDString)
|
debug.Log("dump file %q from %q", pathToPrint, snapshotIDString)
|
||||||
|
|
||||||
splittedPath := splitPath(path.Clean(pathToPrint))
|
splittedPath := splitPath(pathToPrint)
|
||||||
|
|
||||||
repo, err := OpenRepository(gopts)
|
repo, err := OpenRepository(gopts)
|
||||||
if err != nil {
|
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)
|
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 {
|
if err != nil {
|
||||||
Exitf(2, "cannot dump file: %v", err)
|
Exitf(2, "cannot dump file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
+31
-306
@@ -16,38 +16,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var cmdFind = &cobra.Command{
|
var cmdFind = &cobra.Command{
|
||||||
Use: "find [flags] PATTERN...",
|
Use: "find [flags] PATTERN",
|
||||||
Short: "Find a file, a directory or restic IDs",
|
Short: "Find a file or directory",
|
||||||
Long: `
|
Long: `
|
||||||
The "find" command searches for files or directories in snapshots stored in the
|
The "find" command searches for files or directories in snapshots stored in the
|
||||||
repo.
|
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`,
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runFind(findOptions, globalOptions, args)
|
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.
|
// FindOptions bundles all options for the find command.
|
||||||
type FindOptions struct {
|
type FindOptions struct {
|
||||||
Oldest string
|
Oldest string
|
||||||
Newest string
|
Newest string
|
||||||
Snapshots []string
|
Snapshots []string
|
||||||
BlobID, TreeID bool
|
CaseInsensitive bool
|
||||||
PackID, ShowPackID bool
|
ListLong bool
|
||||||
CaseInsensitive bool
|
Host string
|
||||||
ListLong bool
|
Paths []string
|
||||||
Host string
|
Tags restic.TagLists
|
||||||
Paths []string
|
|
||||||
Tags restic.TagLists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var findOptions FindOptions
|
var findOptions FindOptions
|
||||||
@@ -59,10 +48,6 @@ func init() {
|
|||||||
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
|
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
|
||||||
f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest 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.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.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")
|
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 {
|
type findPattern struct {
|
||||||
oldest, newest time.Time
|
oldest, newest time.Time
|
||||||
pattern []string
|
pattern string
|
||||||
ignoreCase bool
|
ignoreCase bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +95,7 @@ type statefulOutput struct {
|
|||||||
hits int
|
hits int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
func (s *statefulOutput) PrintJSON(path string, node *restic.Node) {
|
||||||
type findNode restic.Node
|
type findNode restic.Node
|
||||||
b, err := json.Marshal(struct {
|
b, err := json.Marshal(struct {
|
||||||
// Add these attributes
|
// Add these attributes
|
||||||
@@ -154,7 +139,7 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
|||||||
s.hits++
|
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.newsn != s.oldsn {
|
||||||
if s.oldsn != nil {
|
if s.oldsn != nil {
|
||||||
Verbosef("\n")
|
Verbosef("\n")
|
||||||
@@ -165,62 +150,11 @@ func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) {
|
|||||||
Printf(formatNode(path, node, s.ListLong) + "\n")
|
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 {
|
if s.JSON {
|
||||||
s.PrintPatternJSON(path, node)
|
s.PrintJSON(path, node)
|
||||||
} else {
|
} else {
|
||||||
s.PrintPatternNormal(path, node)
|
s.PrintNormal(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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,9 +179,6 @@ type Finder struct {
|
|||||||
pat findPattern
|
pat findPattern
|
||||||
out statefulOutput
|
out statefulOutput
|
||||||
ignoreTrees restic.IDSet
|
ignoreTrees restic.IDSet
|
||||||
blobIDs map[string]struct{}
|
|
||||||
treeIDs map[string]struct{}
|
|
||||||
itemsFound int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error {
|
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
|
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 {
|
if err != nil {
|
||||||
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
return false, err
|
||||||
|
|
||||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
|
||||||
|
|
||||||
return false, walker.SkipNode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedNodepath := nodepath
|
name := node.Name
|
||||||
if f.pat.ignoreCase {
|
if f.pat.ignoreCase {
|
||||||
normalizedNodepath = strings.ToLower(nodepath)
|
name = strings.ToLower(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
var foundMatch bool
|
foundMatch, err := filter.Match(f.pat.pattern, nodepath)
|
||||||
|
if err != nil {
|
||||||
for _, pat := range f.pat.pattern {
|
return false, err
|
||||||
found, err := filter.Match(pat, normalizedNodepath)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
foundMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -294,16 +213,9 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
errIfNoMatch error
|
errIfNoMatch error
|
||||||
)
|
)
|
||||||
if node.Type == "dir" {
|
if node.Type == "dir" {
|
||||||
var childMayMatch bool
|
childMayMatch, err := filter.ChildMatch(f.pat.pattern, nodepath)
|
||||||
for _, pat := range f.pat.pattern {
|
if err != nil {
|
||||||
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
|
return false, err
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if mayMatch {
|
|
||||||
childMayMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !childMayMatch {
|
if !childMayMatch {
|
||||||
@@ -329,171 +241,20 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug.Log(" found match\n")
|
debug.Log(" found match\n")
|
||||||
f.out.PrintPattern(nodepath, node)
|
f.out.Print(nodepath, node)
|
||||||
return false, nil
|
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 {
|
func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) != 1 {
|
||||||
return errors.Fatal("wrong number of arguments")
|
return errors.Fatal("wrong number of arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
pat := findPattern{pattern: args}
|
pat := findPattern{pattern: args[0]}
|
||||||
if opts.CaseInsensitive {
|
if opts.CaseInsensitive {
|
||||||
for i := range pat.pattern {
|
pat.pattern = strings.ToLower(pat.pattern)
|
||||||
pat.pattern[i] = strings.ToLower(pat.pattern[i])
|
|
||||||
}
|
|
||||||
pat.ignoreCase = true
|
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)
|
repo, err := OpenRepository(gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -543,40 +296,12 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
|||||||
out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON},
|
out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON},
|
||||||
ignoreTrees: restic.NewIDSet(),
|
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) {
|
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 {
|
if err = f.findInSnapshot(ctx, sn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.out.Finish()
|
f.out.Finish()
|
||||||
|
|
||||||
if opts.ShowPackID && (f.blobIDs != nil || f.treeIDs != nil) {
|
|
||||||
f.findObjectsPacks(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+127
-127
@@ -3,8 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/spf13/cobra"
|
"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.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.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.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)")
|
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, "host", "", "only consider snapshots with the given `host`")
|
||||||
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname`")
|
// Deprecated since 2017-03-07.
|
||||||
f.MarkDeprecated("hostname", "use --host")
|
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.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.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")
|
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
|
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
|
removeSnapshots := 0
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var snapshots restic.Snapshots
|
|
||||||
|
|
||||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
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.
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
// When explicit snapshots args are given, remove them immediately.
|
|
||||||
for _, sn := range snapshots {
|
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
||||||
if err = repo.Backend().Remove(gopts.ctx, h); err != nil {
|
if err = repo.Backend().Remove(gopts.ctx, h); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !gopts.JSON {
|
Verbosef("removed snapshot %v\n", sn.ID().Str())
|
||||||
Verbosef("removed snapshot %v\n", sn.ID().Str())
|
|
||||||
}
|
|
||||||
removeSnapshots++
|
removeSnapshots++
|
||||||
} else {
|
} 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 {
|
||||||
} else {
|
// Determining grouping-keys
|
||||||
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
var tags []string
|
||||||
if err != nil {
|
var hostname string
|
||||||
return err
|
var paths []string
|
||||||
}
|
|
||||||
|
|
||||||
policy := restic.ExpirePolicy{
|
if GroupByTag {
|
||||||
Last: opts.Last,
|
tags = sn.Tags
|
||||||
Hourly: opts.Hourly,
|
sort.StringSlice(tags).Sort()
|
||||||
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 GroupByHost {
|
||||||
|
hostname = sn.Hostname
|
||||||
if !policy.Empty() {
|
}
|
||||||
if !gopts.JSON {
|
if GroupByPath {
|
||||||
Verbosef("Applying Policy: %v\n", policy)
|
paths = sn.Paths
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonGroups []*ForgetGroup
|
sort.StringSlice(sn.Paths).Sort()
|
||||||
|
var k []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
for k, snapshotGroup := range snapshotGroups {
|
k, err = json.Marshal(key{Tags: tags, Hostname: hostname, Paths: paths})
|
||||||
if gopts.Verbose >= 1 && !gopts.JSON {
|
|
||||||
err = PrintSnapshotGroupHeader(gopts.stdout, k)
|
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 {
|
if err != nil {
|
||||||
return err
|
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 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 {
|
if !opts.DryRun {
|
||||||
return pruneRepository(gopts, repo)
|
return pruneRepository(gopts, repo)
|
||||||
}
|
}
|
||||||
@@ -218,28 +243,3 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
return nil
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ var cmdGenerate = &cobra.Command{
|
|||||||
Use: "generate [command]",
|
Use: "generate [command]",
|
||||||
Short: "Generate manual pages and auto-completion files (bash, zsh)",
|
Short: "Generate manual pages and auto-completion files (bash, zsh)",
|
||||||
Long: `
|
Long: `
|
||||||
The "generate" command writes automatically generated files (like the man pages
|
The "generate" command writes automatically generated files like the man pages
|
||||||
and the auto-completion files for bash and zsh).
|
and the auto-completion files for bash and zsh).
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
|||||||
+13
-36
@@ -2,7 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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")
|
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 {
|
func listKeys(ctx context.Context, s *repository.Repository) error {
|
||||||
type keyInfo struct {
|
tab := NewTable()
|
||||||
Current bool `json:"current"`
|
tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created")
|
||||||
ID string `json:"id"`
|
tab.RowFormat = "%s%-10s %-10s %-10s %s"
|
||||||
UserName string `json:"userName"`
|
|
||||||
HostName string `json:"hostName"`
|
|
||||||
Created string `json:"created"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys []keyInfo
|
|
||||||
|
|
||||||
err := s.List(ctx, restic.KeyFile, func(id restic.ID, size int64) error {
|
err := s.List(ctx, restic.KeyFile, func(id restic.ID, size int64) error {
|
||||||
k, err := repository.LoadKey(ctx, s, id.String())
|
k, err := repository.LoadKey(ctx, s, id.String())
|
||||||
@@ -54,36 +47,20 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
key := keyInfo{
|
var current string
|
||||||
Current: id.String() == s.KeyName(),
|
if id.String() == s.KeyName() {
|
||||||
ID: id.Str(),
|
current = "*"
|
||||||
UserName: k.Username,
|
} else {
|
||||||
HostName: k.Hostname,
|
current = " "
|
||||||
Created: k.Created.Local().Format(TimeFormat),
|
|
||||||
}
|
}
|
||||||
|
tab.Rows = append(tab.Rows, []interface{}{current, id.Str(),
|
||||||
keys = append(keys, key)
|
k.Username, k.Hostname, k.Created.Format(TimeFormat)})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
return tab.Write(globalOptions.stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +160,7 @@ func runKey(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return listKeys(ctx, repo, gopts)
|
return listKeys(ctx, repo)
|
||||||
case "add":
|
case "add":
|
||||||
lock, err := lockRepo(repo)
|
lock, err := lockRepo(repo)
|
||||||
defer unlockRepo(lock)
|
defer unlockRepo(lock)
|
||||||
|
|||||||
+13
-161
@@ -2,37 +2,21 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdLs = &cobra.Command{
|
var cmdLs = &cobra.Command{
|
||||||
Use: "ls [flags] [snapshotID] [dir...]",
|
Use: "ls [flags] [snapshot-ID ...]",
|
||||||
Short: "List files in a snapshot",
|
Short: "List files in a snapshot",
|
||||||
Long: `
|
Long: `
|
||||||
The "ls" command lists files and directories in a snapshot.
|
The "ls" command allows listing files and directories in a snapshot.
|
||||||
|
|
||||||
The special snapshot ID "latest" can be used to list files and
|
The special snapshot-ID "latest" can be used to list files and directories of the latest snapshot in the repository.
|
||||||
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.
|
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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.
|
// LsOptions collects all options for the ls command.
|
||||||
type LsOptions struct {
|
type LsOptions struct {
|
||||||
ListLong bool
|
ListLong bool
|
||||||
Host string
|
Host string
|
||||||
Tags restic.TagLists
|
Tags restic.TagLists
|
||||||
Paths []string
|
Paths []string
|
||||||
Recursive bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var lsOptions LsOptions
|
var lsOptions LsOptions
|
||||||
@@ -56,31 +39,10 @@ func init() {
|
|||||||
|
|
||||||
flags := cmdLs.Flags()
|
flags := cmdLs.Flags()
|
||||||
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
|
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.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.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.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 {
|
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.")
|
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)
|
repo, err := OpenRepository(gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -144,88 +61,23 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||||
defer cancel()
|
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 (
|
err := walker.Walk(ctx, repo, *sn.Tree, nil, func(nodepath string, node *restic.Node, err error) (bool, error) {
|
||||||
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) {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
Printf("%s\n", formatNode(nodepath, node, lsOptions.ListLong))
|
||||||
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
|
|
||||||
}
|
|
||||||
return false, nil
|
return false, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-16
@@ -1,4 +1,3 @@
|
|||||||
// +build !netbsd
|
|
||||||
// +build !openbsd
|
// +build !openbsd
|
||||||
// +build !solaris
|
// +build !solaris
|
||||||
// +build !windows
|
// +build !windows
|
||||||
@@ -53,14 +52,13 @@ For details please see the documentation for time.Format() at:
|
|||||||
|
|
||||||
// MountOptions collects all options for the mount command.
|
// MountOptions collects all options for the mount command.
|
||||||
type MountOptions struct {
|
type MountOptions struct {
|
||||||
OwnerRoot bool
|
OwnerRoot bool
|
||||||
AllowRoot bool
|
AllowRoot bool
|
||||||
AllowOther bool
|
AllowOther bool
|
||||||
NoDefaultPermissions bool
|
Host string
|
||||||
Host string
|
Tags restic.TagLists
|
||||||
Tags restic.TagLists
|
Paths []string
|
||||||
Paths []string
|
SnapshotTemplate string
|
||||||
SnapshotTemplate string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var mountOptions MountOptions
|
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.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.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.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.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`)
|
||||||
mountFlags.Var(&mountOptions.Tags, "tag", "only consider snapshots which include this `taglist`")
|
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 {
|
if opts.AllowOther {
|
||||||
mountOptions = append(mountOptions, systemFuse.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...)
|
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("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)
|
debug.Log("serving mount at %v", mountpoint)
|
||||||
err = fs.Serve(c, root)
|
err = fs.Serve(c, root)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/options"
|
"github.com/restic/restic/internal/ui/options"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
|||||||
len(idx.Packs), blobs, formatBytes(uint64(stats.bytes)))
|
len(idx.Packs), blobs, formatBytes(uint64(stats.bytes)))
|
||||||
|
|
||||||
blobCount := make(map[restic.BlobHandle]int)
|
blobCount := make(map[restic.BlobHandle]int)
|
||||||
var duplicateBlobs uint64
|
duplicateBlobs := 0
|
||||||
var duplicateBytes uint64
|
duplicateBytes := 0
|
||||||
|
|
||||||
// find duplicate blobs
|
// find duplicate blobs
|
||||||
for _, p := range idx.Packs {
|
for _, p := range idx.Packs {
|
||||||
@@ -161,7 +161,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
|||||||
|
|
||||||
if blobCount[h] > 1 {
|
if blobCount[h] > 1 {
|
||||||
duplicateBlobs++
|
duplicateBlobs++
|
||||||
duplicateBytes += uint64(entry.Length)
|
duplicateBytes += int(entry.Length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
removeBytes += uint64(blob.Length)
|
removeBytes += int(blob.Length)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasActiveBlob {
|
if hasActiveBlob {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
+15
-49
@@ -5,8 +5,6 @@ import (
|
|||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/filter"
|
"github.com/restic/restic/internal/filter"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/restorer"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -29,15 +27,12 @@ repository.
|
|||||||
|
|
||||||
// RestoreOptions collects all options for the restore command.
|
// RestoreOptions collects all options for the restore command.
|
||||||
type RestoreOptions struct {
|
type RestoreOptions struct {
|
||||||
Exclude []string
|
Exclude []string
|
||||||
InsensitiveExclude []string
|
Include []string
|
||||||
Include []string
|
Target string
|
||||||
InsensitiveInclude []string
|
Host string
|
||||||
Target string
|
Paths []string
|
||||||
Host string
|
Tags restic.TagLists
|
||||||
Paths []string
|
|
||||||
Tags restic.TagLists
|
|
||||||
Verify bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var restoreOptions RestoreOptions
|
var restoreOptions RestoreOptions
|
||||||
@@ -47,29 +42,16 @@ func init() {
|
|||||||
|
|
||||||
flags := cmdRestore.Flags()
|
flags := cmdRestore.Flags()
|
||||||
flags.StringArrayVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
|
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.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.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.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.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.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 {
|
func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||||
ctx := gopts.ctx
|
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 {
|
switch {
|
||||||
case len(args) == 0:
|
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)")
|
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")
|
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 {
|
if err != nil {
|
||||||
Exitf(2, "creating restorer failed: %v\n", err)
|
Exitf(2, "creating restorer failed: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalErrors := 0
|
totalErrors := 0
|
||||||
res.Error = func(location string, err error) error {
|
res.Error = func(dir string, node *restic.Node, err error) error {
|
||||||
Warnf("ignoring error for %s: %s\n", location, err)
|
Warnf("ignoring error for %s: %s\n", dir, err)
|
||||||
totalErrors++
|
totalErrors++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -140,16 +122,11 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
|||||||
Warnf("error for exclude pattern: %v", err)
|
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',
|
// An exclude filter is basically a 'wildcard but foo',
|
||||||
// so even if a childMayMatch, other children of a dir may not,
|
// so even if a childMayMatch, other children of a dir may not,
|
||||||
// therefore childMayMatch does not matter, but we should not go down
|
// therefore childMayMatch does not matter, but we should not go down
|
||||||
// unless the dir is selected for restore
|
// unless the dir is selected for restore
|
||||||
selectedForRestore = !matched && !matchedInsensitive
|
selectedForRestore = !matched
|
||||||
childMayBeSelected = selectedForRestore && node.Type == "dir"
|
childMayBeSelected = selectedForRestore && node.Type == "dir"
|
||||||
|
|
||||||
return selectedForRestore, childMayBeSelected
|
return selectedForRestore, childMayBeSelected
|
||||||
@@ -161,32 +138,21 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
|||||||
Warnf("error for include pattern: %v", err)
|
Warnf("error for include pattern: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
matchedInsensitive, childMayMatchInsensitive, err := filter.List(opts.InsensitiveInclude, strings.ToLower(item))
|
selectedForRestore = matched
|
||||||
if err != nil {
|
childMayBeSelected = childMayMatch && node.Type == "dir"
|
||||||
Warnf("error for iexclude pattern: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedForRestore = matched || matchedInsensitive
|
|
||||||
childMayBeSelected = (childMayMatch || childMayMatchInsensitive) && node.Type == "dir"
|
|
||||||
|
|
||||||
return selectedForRestore, childMayBeSelected
|
return selectedForRestore, childMayBeSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasExcludes {
|
if len(opts.Exclude) > 0 {
|
||||||
res.SelectFilter = selectExcludeFilter
|
res.SelectFilter = selectExcludeFilter
|
||||||
} else if hasIncludes {
|
} else if len(opts.Include) > 0 {
|
||||||
res.SelectFilter = selectIncludeFilter
|
res.SelectFilter = selectIncludeFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
||||||
|
|
||||||
err = res.RestoreTo(ctx, 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 {
|
if totalErrors > 0 {
|
||||||
Printf("There were %d errors\n", totalErrors)
|
Printf("There were %d errors\n", totalErrors)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
+69
-173
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +31,6 @@ type SnapshotOptions struct {
|
|||||||
Paths []string
|
Paths []string
|
||||||
Compact bool
|
Compact bool
|
||||||
Last bool
|
Last bool
|
||||||
GroupBy string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshotOptions SnapshotOptions
|
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.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.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.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 {
|
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)
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var snapshots restic.Snapshots
|
var list restic.Snapshots
|
||||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||||
snapshots = append(snapshots, sn)
|
list = append(list, sn)
|
||||||
}
|
|
||||||
snapshotGroups, grouped, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, list := range snapshotGroups {
|
if opts.Last {
|
||||||
if opts.Last {
|
list = FilterLastSnapshots(list)
|
||||||
list = FilterLastSnapshots(list)
|
|
||||||
}
|
|
||||||
sort.Sort(sort.Reverse(list))
|
|
||||||
snapshotGroups[k] = list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Sort(sort.Reverse(list))
|
||||||
|
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, grouped)
|
err := printSnapshotsJSON(gopts.stdout, list)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error printing snapshots: %v\n", err)
|
Warnf("error printing snapshot: %v\n", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
PrintSnapshots(gopts.stdout, list, opts.Compact)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
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.
|
// 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) {
|
func PrintSnapshots(stdout io.Writer, list restic.Snapshots, 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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// always sort the snapshots so that the newer ones are listed last
|
// always sort the snapshots so that the newer ones are listed last
|
||||||
sort.SliceStable(list, func(i, j int) bool {
|
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()
|
tab := NewTable()
|
||||||
|
if !compact {
|
||||||
if compact {
|
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s %-3s %s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags", "", "Directory")
|
||||||
tab.AddColumn("ID", "{{ .ID }}")
|
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%%ds %%-3s %%s", -maxHost, -maxTag)
|
||||||
tab.AddColumn("Time", "{{ .Timestamp }}")
|
|
||||||
tab.AddColumn("Host", "{{ .Hostname }}")
|
|
||||||
tab.AddColumn("Tags ", `{{ join .Tags "\n" }}`)
|
|
||||||
} else {
|
} else {
|
||||||
tab.AddColumn("ID", "{{ .ID }}")
|
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags")
|
||||||
tab.AddColumn("Time", "{{ .Timestamp }}")
|
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%s", -maxHost)
|
||||||
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" }}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type snapshot struct {
|
|
||||||
ID string
|
|
||||||
Timestamp string
|
|
||||||
Hostname string
|
|
||||||
Tags []string
|
|
||||||
Reasons []string
|
|
||||||
Paths []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var multiline bool
|
|
||||||
for _, sn := range list {
|
for _, sn := range list {
|
||||||
data := snapshot{
|
if len(sn.Paths) == 0 {
|
||||||
ID: sn.ID().Str(),
|
continue
|
||||||
Timestamp: sn.Time.Local().Format(TimeFormat),
|
|
||||||
Hostname: sn.Hostname,
|
|
||||||
Tags: sn.Tags,
|
|
||||||
Paths: sn.Paths,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(reasons) > 0 {
|
firstTag := ""
|
||||||
id := sn.ID()
|
if len(sn.Tags) > 0 {
|
||||||
data.Reasons = keepReasons[*id].Matches
|
firstTag = sn.Tags[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sn.Paths) > 1 && !compact {
|
rows := len(sn.Paths)
|
||||||
multiline = true
|
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 !compact {
|
||||||
|
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, treeElement, sn.Paths[0]})
|
||||||
if multiline {
|
} else {
|
||||||
// print an additional blank line between snapshots
|
allTags := ""
|
||||||
|
for _, tag := range sn.Tags {
|
||||||
var last int
|
allTags += tag + " "
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
last = idx
|
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, allTags})
|
||||||
return err
|
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)
|
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.
|
// Snapshot helps to print Snaphots as JSON with their ID included.
|
||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
*restic.Snapshot
|
*restic.Snapshot
|
||||||
@@ -285,58 +220,19 @@ type Snapshot struct {
|
|||||||
ShortID string `json:"short_id"`
|
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.
|
// printSnapshotsJSON writes the JSON representation of list to stdout.
|
||||||
func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]restic.Snapshots, grouped bool) error {
|
func printSnapshotsJSON(stdout io.Writer, list restic.Snapshots) error {
|
||||||
if grouped {
|
|
||||||
var snapshotGroups []SnapshotGroup
|
|
||||||
|
|
||||||
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
|
var snapshots []Snapshot
|
||||||
|
|
||||||
for _, list := range snGroups {
|
for _, sn := range list {
|
||||||
for _, sn := range list {
|
|
||||||
k := Snapshot{
|
k := Snapshot{
|
||||||
Snapshot: sn,
|
Snapshot: sn,
|
||||||
ID: sn.ID(),
|
ID: sn.ID(),
|
||||||
ShortID: sn.ID().Str(),
|
ShortID: sn.ID().Str(),
|
||||||
}
|
|
||||||
snapshots = append(snapshots, k)
|
|
||||||
}
|
}
|
||||||
|
snapshots = append(snapshots, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.NewEncoder(stdout).Encode(snapshots)
|
return json.NewEncoder(stdout).Encode(snapshots)
|
||||||
|
|||||||
@@ -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"
|
|
||||||
)
|
|
||||||
+12
-29
@@ -60,20 +60,15 @@ func (rc *rejectionCache) Store(dir string, rejected bool) {
|
|||||||
rc.m[dir] = rejected
|
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
|
// 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
|
// file that would be included in the backup. The function returns true if it
|
||||||
// should be excluded (rejected) from the backup.
|
// should be excluded (rejected) from the backup.
|
||||||
type RejectFunc func(path string, fi os.FileInfo) bool
|
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.
|
// one of the patterns.
|
||||||
func rejectByPattern(patterns []string) RejectByNameFunc {
|
func rejectByPattern(patterns []string) RejectFunc {
|
||||||
return func(item string) bool {
|
return func(item string, fi os.FileInfo) bool {
|
||||||
matched, _, err := filter.List(patterns, item)
|
matched, _, err := filter.List(patterns, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error for exclude pattern: %v", err)
|
Warnf("error for exclude pattern: %v", err)
|
||||||
@@ -88,26 +83,14 @@ func rejectByPattern(patterns []string) RejectByNameFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same as `rejectByPattern` but case insensitive.
|
// rejectIfPresent returns a RejectFunc which itself returns whether a path
|
||||||
func rejectByInsensitivePattern(patterns []string) RejectByNameFunc {
|
// should be excluded. The RejectFunc considers a file to be excluded when
|
||||||
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
|
|
||||||
// it resides in a directory with an exclusion file, that is specified by
|
// it resides in a directory with an exclusion file, that is specified by
|
||||||
// excludeFileSpec in the form "filename[:content]". The returned error is
|
// 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 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.
|
// of a directory based on previous visits.
|
||||||
func rejectIfPresent(excludeFileSpec string) (RejectByNameFunc, error) {
|
func rejectIfPresent(excludeFileSpec string) (RejectFunc, error) {
|
||||||
if excludeFileSpec == "" {
|
if excludeFileSpec == "" {
|
||||||
return nil, errors.New("name for exclusion tagfile is empty")
|
return nil, errors.New("name for exclusion tagfile is empty")
|
||||||
}
|
}
|
||||||
@@ -124,7 +107,7 @@ func rejectIfPresent(excludeFileSpec string) (RejectByNameFunc, error) {
|
|||||||
}
|
}
|
||||||
debug.Log("using %q as exclusion tagfile", tf)
|
debug.Log("using %q as exclusion tagfile", tf)
|
||||||
rc := &rejectionCache{}
|
rc := &rejectionCache{}
|
||||||
fn := func(filename string) bool {
|
fn := func(filename string, _ os.FileInfo) bool {
|
||||||
return isExcludedByFile(filename, tf, tc, rc)
|
return isExcludedByFile(filename, tf, tc, rc)
|
||||||
}
|
}
|
||||||
return fn, nil
|
return fn, nil
|
||||||
@@ -269,11 +252,11 @@ func rejectByDevice(samples []string) (RejectFunc, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// rejectResticCache returns a RejectByNameFunc that rejects the restic cache
|
// rejectResticCache returns a RejectFunc that rejects the restic cache
|
||||||
// directory (if set).
|
// directory (if set).
|
||||||
func rejectResticCache(repo *repository.Repository) (RejectByNameFunc, error) {
|
func rejectResticCache(repo *repository.Repository) (RejectFunc, error) {
|
||||||
if repo.Cache == nil {
|
if repo.Cache == nil {
|
||||||
return func(string) bool {
|
return func(string, os.FileInfo) bool {
|
||||||
return false
|
return false
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -283,7 +266,7 @@ func rejectResticCache(repo *repository.Repository) (RejectByNameFunc, error) {
|
|||||||
return nil, errors.New("cacheBase is empty string")
|
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) {
|
if fs.HasPathPrefix(cacheBase, item) {
|
||||||
debug.Log("rejecting restic cache directory %v", item)
|
debug.Log("rejecting restic cache directory %v", item)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -27,34 +27,7 @@ func TestRejectByPattern(t *testing.T) {
|
|||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
reject := rejectByPattern(patterns)
|
reject := rejectByPattern(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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
if res != tc.reject {
|
if res != tc.reject {
|
||||||
t.Fatalf("wrong result for filename %v: want %v, got %v",
|
t.Fatalf("wrong result for filename %v: want %v, got %v",
|
||||||
tc.filename, tc.reject, res)
|
tc.filename, tc.reject, res)
|
||||||
@@ -167,8 +140,8 @@ func TestMultipleIsExcludedByFile(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
excludedByFoo := fooExclude(p)
|
excludedByFoo := fooExclude(p, fi)
|
||||||
excludedByBar := barExclude(p)
|
excludedByBar := barExclude(p, fi)
|
||||||
excluded := excludedByFoo || excludedByBar
|
excluded := excludedByFoo || excludedByBar
|
||||||
// the log message helps debugging in case the test fails
|
// the log message helps debugging in case the test fails
|
||||||
t.Logf("%q: %v || %v = %v", p, excludedByFoo, excludedByBar, excluded)
|
t.Logf("%q: %v || %v = %v", p, excludedByFoo, excludedByBar, excluded)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func formatBytes(c uint64) string {
|
|||||||
case c > 1<<10:
|
case c > 1<<10:
|
||||||
return fmt.Sprintf("%.3f KiB", b/(1<<10))
|
return fmt.Sprintf("%.3f KiB", b/(1<<10))
|
||||||
default:
|
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",
|
return fmt.Sprintf("%s %5d %5d %6d %s %s%s",
|
||||||
mode|n.Mode, n.UID, n.GID, n.Size,
|
mode|n.Mode, n.UID, n.GID, n.Size,
|
||||||
n.ModTime.Local().Format(TimeFormat), path,
|
n.ModTime.Format(TimeFormat), path,
|
||||||
target)
|
target)
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-97
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -27,38 +26,32 @@ import (
|
|||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
"github.com/restic/restic/internal/limiter"
|
"github.com/restic/restic/internal/limiter"
|
||||||
"github.com/restic/restic/internal/options"
|
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/textfile"
|
"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"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.9.6"
|
var version = "compiled manually"
|
||||||
|
|
||||||
// TimeFormat is the format used for all timestamps printed by restic.
|
|
||||||
const TimeFormat = "2006-01-02 15:04:05"
|
|
||||||
|
|
||||||
// GlobalOptions hold all global options for restic.
|
// GlobalOptions hold all global options for restic.
|
||||||
type GlobalOptions struct {
|
type GlobalOptions struct {
|
||||||
Repo string
|
config.Config
|
||||||
PasswordFile string
|
|
||||||
PasswordCommand string
|
Quiet bool
|
||||||
KeyHint string
|
Verbose int
|
||||||
Quiet bool
|
NoLock bool
|
||||||
Verbose int
|
JSON bool
|
||||||
NoLock bool
|
CacheDir string
|
||||||
JSON bool
|
NoCache bool
|
||||||
CacheDir string
|
CACerts []string
|
||||||
NoCache bool
|
TLSClientCert string
|
||||||
CACerts []string
|
CleanupCache bool
|
||||||
TLSClientCert string
|
|
||||||
CleanupCache bool
|
|
||||||
|
|
||||||
LimitUploadKb int
|
LimitUploadKb int
|
||||||
LimitDownloadKb 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
|
// 0 means: don't print any messages except errors, this is used when --quiet is specified
|
||||||
// 1 is the default: print essential messages
|
// 1 is the default: print essential messages
|
||||||
// 2 means: print more messages, report minor things, this is used when --verbose is specified
|
// 2 means: print more messages, report minor things, this is used when --verbose is specified
|
||||||
// 3 means: print very detailed debug messages, this is used when --verbose 2 is specified
|
// 3 means: print very detailed debug messages, this is used when --debug is specified
|
||||||
verbosity uint
|
verbosity uint
|
||||||
|
|
||||||
Options []string
|
Options []string
|
||||||
@@ -94,15 +87,16 @@ func init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
f := cmdRoot.PersistentFlags()
|
f := cmdRoot.PersistentFlags()
|
||||||
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
|
||||||
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
|
// these fields are embedded in config.Config and queried via f.Get[...]()
|
||||||
f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
|
f.StringP("repo", "r", "", "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
||||||
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "specify a shell command to obtain a password (default: $RESTIC_PASSWORD_COMMAND)")
|
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.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.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
|
||||||
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
|
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
|
||||||
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
|
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
|
||||||
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory. (default: use system default cache directory)")
|
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory")
|
||||||
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
|
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.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")
|
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...)
|
_, err := fmt.Fprintf(globalOptions.stdout, format, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
|
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...)
|
_, err := fmt.Fprintf(globalOptions.stderr, format, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err)
|
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.
|
// resolvePassword determines the password to be used for opening the repository.
|
||||||
func resolvePassword(opts GlobalOptions) (string, error) {
|
func resolvePassword(opts GlobalOptions) (string, error) {
|
||||||
if opts.PasswordFile != "" && opts.PasswordCommand != "" {
|
if opts.Password != "" {
|
||||||
return "", errors.Fatalf("Password file and command are mutually exclusive options")
|
return opts.Password, nil
|
||||||
}
|
|
||||||
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.PasswordFile != "" {
|
if opts.PasswordFile != "" {
|
||||||
s, err := textfile.Read(opts.PasswordFile)
|
s, err := textfile.Read(opts.PasswordFile)
|
||||||
if os.IsNotExist(errors.Cause(err)) {
|
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")
|
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
||||||
}
|
}
|
||||||
|
|
||||||
if pwd := os.Getenv("RESTIC_PASSWORD"); pwd != "" {
|
|
||||||
return pwd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPassword reads the password from the given reader directly.
|
// readPassword reads the password from the given reader directly.
|
||||||
func readPassword(in io.Reader) (password string, err error) {
|
func readPassword(in io.Reader) (password string, err error) {
|
||||||
sc := bufio.NewScanner(in)
|
buf := make([]byte, 1000)
|
||||||
sc.Scan()
|
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
|
// 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)
|
password, err = readPasswordTerminal(os.Stdin, os.Stderr, prompt)
|
||||||
} else {
|
} else {
|
||||||
password, err = readPassword(os.Stdin)
|
password, err = readPassword(os.Stdin)
|
||||||
Verbosef("read password from stdin\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -320,7 +304,7 @@ func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(password) == 0 {
|
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
|
return password, nil
|
||||||
@@ -333,15 +317,13 @@ func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if stdinIsTerminal() {
|
pw2, err := ReadPassword(gopts, prompt2)
|
||||||
pw2, err := ReadPassword(gopts, prompt2)
|
if err != nil {
|
||||||
if err != nil {
|
return "", err
|
||||||
return "", err
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if pw1 != pw2 {
|
if pw1 != pw2 {
|
||||||
return "", errors.Fatal("passwords do not match")
|
return "", errors.Fatal("passwords do not match")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pw1, nil
|
return pw1, nil
|
||||||
@@ -366,42 +348,22 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
|||||||
|
|
||||||
s := repository.New(be)
|
s := repository.New(be)
|
||||||
|
|
||||||
passwordTriesLeft := 1
|
opts.password, err = ReadPassword(opts, "enter password for repository: ")
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsFatal(err) {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, errors.Fatalf("%s", 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
|
id := s.Config().ID
|
||||||
if len(id) > 8 {
|
if len(id) > 8 {
|
||||||
id = 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 {
|
if opts.NoCache {
|
||||||
@@ -414,10 +376,6 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Created && !opts.JSON {
|
|
||||||
Verbosef("created new cache in %v\n", c.Base)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start using the cache
|
// start using the cache
|
||||||
s.UseCache(c)
|
s.UseCache(c)
|
||||||
|
|
||||||
@@ -444,7 +402,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if stdoutIsTerminal() {
|
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)
|
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")
|
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 {
|
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// +build !netbsd
|
|
||||||
// +build !openbsd
|
// +build !openbsd
|
||||||
// +build !solaris
|
// +build !solaris
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/options"
|
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
"github.com/restic/restic/internal/ui/config"
|
||||||
|
"github.com/restic/restic/internal/ui/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dirEntry struct {
|
type dirEntry struct {
|
||||||
@@ -66,7 +67,7 @@ func isSymlink(fi os.FileInfo) bool {
|
|||||||
|
|
||||||
func sameModTime(fi1, fi2 os.FileInfo) bool {
|
func sameModTime(fi1, fi2 os.FileInfo) bool {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
case "darwin", "freebsd", "openbsd":
|
||||||
if isSymlink(fi1) && isSymlink(fi2) {
|
if isSymlink(fi1) && isSymlink(fi2) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -209,7 +210,9 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
|
|||||||
rtest.OK(t, os.MkdirAll(env.repo, 0700))
|
rtest.OK(t, os.MkdirAll(env.repo, 0700))
|
||||||
|
|
||||||
env.gopts = GlobalOptions{
|
env.gopts = GlobalOptions{
|
||||||
Repo: env.repo,
|
Config: config.Config{
|
||||||
|
Repo: env.repo,
|
||||||
|
},
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
CacheDir: env.cache,
|
CacheDir: env.cache,
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user