mirror of
https://github.com/restic/restic.git
synced 2026-02-23 09:16:24 +00:00
Compare commits
194 Commits
v0.9.6
...
debug-chun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e72d638df | ||
|
|
070d43e290 | ||
|
|
d4bd32a37e | ||
|
|
e7d7b85d59 | ||
|
|
be5a0ff59f | ||
|
|
c5100d5632 | ||
|
|
956a1b0f96 | ||
|
|
fab626a3df | ||
|
|
4f00564574 | ||
|
|
f77477129f | ||
|
|
2e31120f89 | ||
|
|
8fb2c0d3c1 | ||
|
|
072cf7b02d | ||
|
|
df66daa5c9 | ||
|
|
9790d8ce1c | ||
|
|
16710454f4 | ||
|
|
08ec6c9f17 | ||
|
|
7910ff4c0e | ||
|
|
c4da9d1e90 | ||
|
|
3ed61987a2 | ||
|
|
9dba7a2577 | ||
|
|
a1352906e2 | ||
|
|
b7c0d4d8bf | ||
|
|
f033850aa0 | ||
|
|
bdf7ba20cb | ||
|
|
3ee6b8ec63 | ||
|
|
4a400f94bb | ||
|
|
1a1c572bac | ||
|
|
5a7c27ddb6 | ||
|
|
fb842759fc | ||
|
|
7aa2f8a61e | ||
|
|
08bf3bae79 | ||
|
|
e7b741b2d7 | ||
|
|
6bee62e346 | ||
|
|
bc74cd3ae5 | ||
|
|
90243ed1c4 | ||
|
|
0ce81d88b6 | ||
|
|
c03bc88b29 | ||
|
|
b8da7b1f4d | ||
|
|
f1b4d97945 | ||
|
|
2b5a6d255a | ||
|
|
f004dbe605 | ||
|
|
9efbe98879 | ||
|
|
7d9300efca | ||
|
|
c38aaaa768 | ||
|
|
8941041355 | ||
|
|
18fee4806f | ||
|
|
47d4d5bf1b | ||
|
|
74a64c47e4 | ||
|
|
a23e9c86ba | ||
|
|
4de12bf593 | ||
|
|
c542a509f0 | ||
|
|
8cf3bb8737 | ||
|
|
b46cc6d57e | ||
|
|
4a2156d3f0 | ||
|
|
41fee11f66 | ||
|
|
b592614061 | ||
|
|
b7c3039eb2 | ||
|
|
a307797c11 | ||
|
|
a03f107144 | ||
|
|
a141ab1bda | ||
|
|
52abec967f | ||
|
|
2828a9c2b0 | ||
|
|
cec7d581f3 | ||
|
|
12aa1e61da | ||
|
|
95da6c1c1d | ||
|
|
0c03a80fc4 | ||
|
|
b1c77172c2 | ||
|
|
c0373cd307 | ||
|
|
28121090c2 | ||
|
|
44a57d66c3 | ||
|
|
266f9dbe16 | ||
|
|
60e4a88f17 | ||
|
|
c50f91b7f9 | ||
|
|
e851d29565 | ||
|
|
b67b7ebfe6 | ||
|
|
751eba0e68 | ||
|
|
7447c44484 | ||
|
|
c8a672fa29 | ||
|
|
863ba76494 | ||
|
|
8526cc6647 | ||
|
|
694b7a17e7 | ||
|
|
5cd0bce452 | ||
|
|
58bd165253 | ||
|
|
65d3fb6b33 | ||
|
|
f165048172 | ||
|
|
de5516a90e | ||
|
|
4f6fd9fb98 | ||
|
|
9a9101d144 | ||
|
|
616f9499ae | ||
|
|
a40ac37550 | ||
|
|
99fd80a585 | ||
|
|
2464f7c4d1 | ||
|
|
b5c7778428 | ||
|
|
c52198d12c | ||
|
|
f17ffa0283 | ||
|
|
5e2afd91e7 | ||
|
|
79b882e901 | ||
|
|
1b502fa9ef | ||
|
|
e1969d1e33 | ||
|
|
6ac6bca7a1 | ||
|
|
3a6feb0596 | ||
|
|
2f8aa2ce30 | ||
|
|
f2bf06a419 | ||
|
|
71900248ab | ||
|
|
23055aaadf | ||
|
|
8bf6a3af97 | ||
|
|
27d6e5e186 | ||
|
|
4214b1746e | ||
|
|
f6f240573a | ||
|
|
ecdf49679e | ||
|
|
e1f722d266 | ||
|
|
2d47381b1d | ||
|
|
294ca55967 | ||
|
|
78c518ccac | ||
|
|
42a3292bcf | ||
|
|
ef70a2fcb3 | ||
|
|
af20015725 | ||
|
|
680a14afa1 | ||
|
|
bf199e5ca7 | ||
|
|
ae127a412d | ||
|
|
5ae7e6f945 | ||
|
|
d8da9c4401 | ||
|
|
daf3bfc882 | ||
|
|
94f4f13388 | ||
|
|
4615bdfd70 | ||
|
|
299f5971f2 | ||
|
|
7a1352ae87 | ||
|
|
72734d59b5 | ||
|
|
3679669aae | ||
|
|
3ed54e762e | ||
|
|
fea835b4e2 | ||
|
|
16b321b140 | ||
|
|
b58dfda212 | ||
|
|
b229aa5cf1 | ||
|
|
81c0b031f9 | ||
|
|
760863e7f9 | ||
|
|
a135699397 | ||
|
|
94a4d45dfb | ||
|
|
4de384087d | ||
|
|
57815d9cd6 | ||
|
|
644673bcf2 | ||
|
|
e7cdf2acbb | ||
|
|
0f22f008ed | ||
|
|
c184da21d0 | ||
|
|
ef5efc6a82 | ||
|
|
688014487b | ||
|
|
38ddfbc4d3 | ||
|
|
da48b925ff | ||
|
|
4bcd56fbae | ||
|
|
6e9778ae0e | ||
|
|
63c67be908 | ||
|
|
d70a4a9350 | ||
|
|
2cd9c7ef16 | ||
|
|
6ec5dc8016 | ||
|
|
fe430a680a | ||
|
|
69a0d0ee90 | ||
|
|
4557881066 | ||
|
|
9b5d069ade | ||
|
|
c56cbfe95b | ||
|
|
97e5ce4344 | ||
|
|
72bd467e23 | ||
|
|
d818b7618b | ||
|
|
1c3812a6f6 | ||
|
|
ec91b80f09 | ||
|
|
18a336c154 | ||
|
|
d21a13f1ee | ||
|
|
e0eac30ee5 | ||
|
|
fccb579471 | ||
|
|
6c700f95b5 | ||
|
|
95d070c147 | ||
|
|
da4473aba6 | ||
|
|
6e85a58045 | ||
|
|
476e2e8762 | ||
|
|
246bb46e82 | ||
|
|
fe0361ffcb | ||
|
|
952469473f | ||
|
|
02108f202e | ||
|
|
b02357f542 | ||
|
|
f2aeaef8f1 | ||
|
|
547702d285 | ||
|
|
599831f874 | ||
|
|
14c90d9e85 | ||
|
|
c41bbb3761 | ||
|
|
8a54a0f5ac | ||
|
|
c0a5530950 | ||
|
|
77ef92b95c | ||
|
|
69f75bbf70 | ||
|
|
30519f01ff | ||
|
|
7cacba0394 | ||
|
|
1596d06f8e | ||
|
|
db20c0b8d0 | ||
|
|
3ca306d69a | ||
|
|
5d272e5c08 |
27
.github/ISSUE_TEMPLATE.md
vendored
27
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,27 +0,0 @@
|
||||
<!--
|
||||
|
||||
Welcome! If you have a question or are unsure if you should open an issue,
|
||||
please use the forum instead!
|
||||
|
||||
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.
|
||||
-->
|
||||
|
||||
Describe the issue
|
||||
------------------
|
||||
4
.github/ISSUE_TEMPLATE/Bug.md
vendored
4
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -83,8 +83,8 @@ Do you have an idea how to solve the issue?
|
||||
|
||||
|
||||
|
||||
Did restic help you or made you happy in any way?
|
||||
-------------------------------------------------
|
||||
Did restic help you today? Did it make you happy in any way?
|
||||
------------------------------------------------------------
|
||||
|
||||
<!--
|
||||
Answering this question is not required, but if you have anything positive to share, please do so here!
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/Feature.md
vendored
4
.github/ISSUE_TEMPLATE/Feature.md
vendored
@@ -47,8 +47,8 @@ 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?
|
||||
-------------------------------------------------
|
||||
Did restic help you today? Did it make you happy in any way?
|
||||
------------------------------------------------------------
|
||||
|
||||
<!--
|
||||
Answering this question is not required, but if you have anything positive to share, please do so here!
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: restic forum
|
||||
url: https://forum.restic.net
|
||||
about: Please ask questions about using restic here, do not open an issue for questions.
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -31,6 +31,7 @@ Checklist
|
||||
---------
|
||||
|
||||
- [ ] I have read the [Contribution Guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches)
|
||||
- [ ] I have enabled [maintainer edits for this PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)
|
||||
- [ ] I have added tests for all changes in this PR
|
||||
- [ ] I have added documentation for the changes (in the manual)
|
||||
- [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (template [here](https://github.com/restic/restic/blob/master/changelog/TEMPLATE))
|
||||
|
||||
20
.travis.yml
20
.travis.yml
@@ -3,14 +3,6 @@ sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
go: "1.10.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
- os: linux
|
||||
go: "1.11.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
@@ -27,9 +19,17 @@ matrix:
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
|
||||
- os: linux
|
||||
go: "1.13.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
|
||||
- os: linux
|
||||
go: "1.14.x"
|
||||
sudo: true
|
||||
cache:
|
||||
directories:
|
||||
@@ -37,7 +37,7 @@ matrix:
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
- os: osx
|
||||
go: "1.13.x"
|
||||
go: "1.14.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -12,6 +12,7 @@ Summary
|
||||
* 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 #2179: Use ctime when checking for file changes
|
||||
* 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
|
||||
@@ -60,6 +61,21 @@ Details
|
||||
https://github.com/restic/restic/pull/2321
|
||||
https://forum.restic.net/t/check-rebuild-index-prune/1848/13
|
||||
|
||||
* 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 #2306: Allow multiple retries for interactive password input
|
||||
|
||||
Restic used to quit if the repository password was typed incorrectly once. Restic will now ask
|
||||
@@ -101,7 +117,6 @@ Summary
|
||||
* 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
|
||||
@@ -167,21 +182,6 @@ Details
|
||||
|
||||
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
|
||||
|
||||
@@ -46,15 +46,12 @@ Remember, the easier it is for us to reproduce the bug, the earlier it will be
|
||||
corrected!
|
||||
|
||||
In addition, you can compile restic with debug support by running
|
||||
`go run -mod=vendor build.go -tags debug` and instructing it to create a debug
|
||||
`go run build.go -tags debug` and instructing it to create a debug
|
||||
log by setting the environment variable `DEBUG_LOG` to a file, e.g. like this:
|
||||
|
||||
$ export DEBUG_LOG=/tmp/restic-debug.log
|
||||
$ restic backup ~/work
|
||||
|
||||
For Go < 1.11, you need to remove the `-mod=vendor` option from the build
|
||||
command.
|
||||
|
||||
Please be aware that the debug log file will contain potentially sensitive
|
||||
things like file and directory names, so please either redact it before
|
||||
uploading it somewhere or post only the parts that are really relevant.
|
||||
@@ -141,7 +138,7 @@ down to the following steps:
|
||||
4. Push the new branch with your changes to your fork of the repository.
|
||||
|
||||
5. Create a pull request by visiting the GitHub website, it will guide you
|
||||
through the process.
|
||||
through the process. Please [allow edits from maintainers](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
|
||||
|
||||
6. You will receive comments on your code and the feature or bug that they
|
||||
address. Maybe you need to rework some minor things, in this case push new
|
||||
|
||||
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@
|
||||
all: restic
|
||||
|
||||
restic:
|
||||
go run -mod=vendor build.go || go run build.go
|
||||
go run build.go
|
||||
|
||||
clean:
|
||||
rm -f restic
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |TestCoverage| |Reviewed by Hound|
|
||||
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |Reviewed by Hound|
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
@@ -20,8 +20,8 @@ init:
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://dl.google.com/go/go1.13.4.windows-amd64.msi
|
||||
- msiexec /i go1.13.4.windows-amd64.msi /q
|
||||
- appveyor DownloadFile https://dl.google.com/go/go1.14.windows-amd64.msi
|
||||
- msiexec /i go1.14.windows-amd64.msi /q
|
||||
- go version
|
||||
- go env
|
||||
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip
|
||||
@@ -29,4 +29,4 @@ install:
|
||||
- set PATH=bin/;%PATH%
|
||||
|
||||
build_script:
|
||||
- go run -mod=vendor run_integration_tests.go
|
||||
- go run run_integration_tests.go
|
||||
|
||||
273
build.go
273
build.go
@@ -3,22 +3,15 @@
|
||||
// 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.
|
||||
// This program needs Go >= 1.11. It'll use Go modules for compilation. It
|
||||
// builds the package configured as Main in the Config struct.
|
||||
|
||||
// BSD 2-Clause License
|
||||
//
|
||||
// Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
|
||||
// All rights reserved.
|
||||
//
|
||||
// This file has been copied from the repository at:
|
||||
// This file has been derived from the repository at:
|
||||
// https://github.com/fd0/build-go
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
@@ -65,7 +58,7 @@ var config = Config{
|
||||
Main: "./cmd/restic", // package name for the main package
|
||||
DefaultBuildTags: []string{"selfupdate"}, // specify build tags which are always used
|
||||
Tests: []string{"./..."}, // tests to run
|
||||
MinVersion: GoVersion{Major: 1, Minor: 10, Patch: 0}, // minimum Go version supported
|
||||
MinVersion: GoVersion{Major: 1, Minor: 11, Patch: 0}, // minimum Go version supported
|
||||
}
|
||||
|
||||
// Config configures the build.
|
||||
@@ -79,125 +72,13 @@ type Config struct {
|
||||
}
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
keepGopath bool
|
||||
runTests bool
|
||||
enableCGO bool
|
||||
enablePIE bool
|
||||
goVersion = ParseGoVersion(runtime.Version())
|
||||
verbose bool
|
||||
runTests bool
|
||||
enableCGO bool
|
||||
enablePIE bool
|
||||
goVersion = ParseGoVersion(runtime.Version())
|
||||
)
|
||||
|
||||
// copy all Go files in src to dst, creating directories on the fly, so calling
|
||||
//
|
||||
// copy("/tmp/gopath/src/github.com/restic/restic", "/home/u/restic")
|
||||
//
|
||||
// with "/home/u/restic" containing the file "foo.go" yields the following tree
|
||||
// at "/tmp/gopath":
|
||||
//
|
||||
// /tmp/gopath
|
||||
// └── src
|
||||
// └── github.com
|
||||
// └── restic
|
||||
// └── restic
|
||||
// └── foo.go
|
||||
func copy(dst, src string) error {
|
||||
verbosePrintf("copy contents of %v to %v\n", src, dst)
|
||||
return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
|
||||
if name == src {
|
||||
return err
|
||||
}
|
||||
|
||||
if name == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
intermediatePath, err := filepath.Rel(src, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileSrc := filepath.Join(src, intermediatePath)
|
||||
fileDst := filepath.Join(dst, intermediatePath)
|
||||
|
||||
return copyFile(fileDst, fileSrc)
|
||||
})
|
||||
}
|
||||
|
||||
func directoryExists(dirname string) bool {
|
||||
stat, err := os.Stat(dirname)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return stat.IsDir()
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
stat, err := os.Stat(filename)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return stat.Mode().IsRegular()
|
||||
}
|
||||
|
||||
// copyFile creates dst from src, preserving file attributes and timestamps.
|
||||
func copyFile(dst, src string) error {
|
||||
fi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fsrc, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
fmt.Printf("MkdirAll(%v)\n", filepath.Dir(dst))
|
||||
return err
|
||||
}
|
||||
|
||||
fdst, err := os.Create(dst)
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(fdst, fsrc)
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
_ = fdst.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
err = fdst.Close()
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
err = fsrc.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Chmod(dst, fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Chtimes(dst, fi.ModTime(), fi.ModTime())
|
||||
}
|
||||
|
||||
// die prints the message with fmt.Fprintf() to stderr and exits with an error
|
||||
// code.
|
||||
func die(message string, args ...interface{}) {
|
||||
@@ -211,7 +92,6 @@ func showUsage(output io.Writer) {
|
||||
fmt.Fprintf(output, "OPTIONS:\n")
|
||||
fmt.Fprintf(output, " -v --verbose output more messages\n")
|
||||
fmt.Fprintf(output, " -t --tags specify additional build tags\n")
|
||||
fmt.Fprintf(output, " -k --keep-tempdir do not remove the temporary directory after build\n")
|
||||
fmt.Fprintf(output, " -T --test run tests\n")
|
||||
fmt.Fprintf(output, " -o --output set output file name\n")
|
||||
fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n")
|
||||
@@ -219,7 +99,6 @@ func showUsage(output io.Writer) {
|
||||
fmt.Fprintf(output, " --goos value set GOOS for cross-compilation\n")
|
||||
fmt.Fprintf(output, " --goarch value set GOARCH for cross-compilation\n")
|
||||
fmt.Fprintf(output, " --goarm value set GOARM for cross-compilation\n")
|
||||
fmt.Fprintf(output, " --tempdir dir use a specific directory for compilation\n")
|
||||
}
|
||||
|
||||
func verbosePrintf(message string, args ...interface{}) {
|
||||
@@ -230,49 +109,39 @@ func verbosePrintf(message string, args ...interface{}) {
|
||||
fmt.Printf("build: "+message, args...)
|
||||
}
|
||||
|
||||
// cleanEnv returns a clean environment with GOPATH, GOBIN and GO111MODULE
|
||||
// removed (if present).
|
||||
func cleanEnv() (env []string) {
|
||||
removeKeys := map[string]struct{}{
|
||||
"GOPATH": struct{}{},
|
||||
"GOBIN": struct{}{},
|
||||
"GO111MODULE": struct{}{},
|
||||
}
|
||||
|
||||
for _, v := range os.Environ() {
|
||||
data := strings.SplitN(v, "=", 2)
|
||||
name := data[0]
|
||||
|
||||
if _, ok := removeKeys[name]; ok {
|
||||
// printEnv prints Go-relevant environment variables in a nice way using verbosePrintf.
|
||||
func printEnv(env []string) {
|
||||
verbosePrintf("environment (GO*):\n")
|
||||
for _, v := range env {
|
||||
// ignore environment variables which do not start with GO*.
|
||||
if !strings.HasPrefix(v, "GO") {
|
||||
continue
|
||||
}
|
||||
|
||||
env = append(env, v)
|
||||
verbosePrintf(" %s\n", v)
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// build runs "go build args..." with GOPATH set to gopath.
|
||||
func build(cwd string, env map[string]string, args ...string) error {
|
||||
a := []string{"build"}
|
||||
|
||||
if goVersion.AtLeast(GoVersion{1, 10, 0}) {
|
||||
verbosePrintf("Go version is at least 1.10, using new syntax for -gcflags\n")
|
||||
// use new prefix
|
||||
// try to remove all absolute paths from resulting binary
|
||||
if goVersion.AtLeast(GoVersion{1, 13, 0}) {
|
||||
// use the new flag introduced by Go 1.13
|
||||
a = append(a, "-trimpath")
|
||||
} else {
|
||||
// otherwise try to trim as many paths as possible
|
||||
a = append(a, "-asmflags", fmt.Sprintf("all=-trimpath=%s", cwd))
|
||||
a = append(a, "-gcflags", fmt.Sprintf("all=-trimpath=%s", cwd))
|
||||
} else {
|
||||
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", cwd))
|
||||
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", cwd))
|
||||
}
|
||||
|
||||
if enablePIE {
|
||||
a = append(a, "-buildmode=pie")
|
||||
}
|
||||
|
||||
a = append(a, args...)
|
||||
cmd := exec.Command("go", a...)
|
||||
cmd.Env = append(cleanEnv(), "GOPROXY=off")
|
||||
cmd.Env = os.Environ()
|
||||
for k, v := range env {
|
||||
cmd.Env = append(cmd.Env, k+"="+v)
|
||||
}
|
||||
@@ -280,6 +149,8 @@ func build(cwd string, env map[string]string, args ...string) error {
|
||||
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
|
||||
}
|
||||
|
||||
printEnv(cmd.Env)
|
||||
|
||||
cmd.Dir = cwd
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -294,7 +165,7 @@ func build(cwd string, env map[string]string, args ...string) error {
|
||||
func test(cwd string, env map[string]string, args ...string) error {
|
||||
args = append([]string{"test", "-count", "1"}, args...)
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Env = append(cleanEnv(), "GOPROXY=off")
|
||||
cmd.Env = os.Environ()
|
||||
for k, v := range env {
|
||||
cmd.Env = append(cmd.Env, k+"="+v)
|
||||
}
|
||||
@@ -305,6 +176,8 @@ func test(cwd string, env map[string]string, args ...string) error {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
printEnv(cmd.Env)
|
||||
|
||||
verbosePrintf("chdir %q\n", cwd)
|
||||
verbosePrintf("go %q\n", args)
|
||||
|
||||
@@ -454,6 +327,10 @@ func (v GoVersion) String() string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if !goVersion.AtLeast(GoVersion{1, 11, 0}) {
|
||||
die("Go version (%v) is too old, Go <= 1.11 does not support Go Modules\n", goVersion)
|
||||
}
|
||||
|
||||
if !goVersion.AtLeast(config.MinVersion) {
|
||||
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion)
|
||||
os.Exit(1)
|
||||
@@ -464,15 +341,13 @@ func main() {
|
||||
skipNext := false
|
||||
params := os.Args[1:]
|
||||
|
||||
goEnv := map[string]string{}
|
||||
buildEnv := map[string]string{
|
||||
"GOOS": runtime.GOOS,
|
||||
"GOARCH": runtime.GOARCH,
|
||||
"GOARM": "",
|
||||
env := map[string]string{
|
||||
"GO111MODULE": "on", // make sure we build in Module mode
|
||||
"GOOS": runtime.GOOS,
|
||||
"GOARCH": runtime.GOARCH,
|
||||
"GOARM": "",
|
||||
}
|
||||
|
||||
tempdir := ""
|
||||
|
||||
var outputFilename string
|
||||
|
||||
for i, arg := range params {
|
||||
@@ -484,8 +359,6 @@ func main() {
|
||||
switch arg {
|
||||
case "-v", "--verbose":
|
||||
verbose = true
|
||||
case "-k", "--keep-gopath":
|
||||
keepGopath = true
|
||||
case "-t", "-tags", "--tags":
|
||||
if i+1 >= len(params) {
|
||||
die("-t given but no tag specified")
|
||||
@@ -495,9 +368,6 @@ func main() {
|
||||
case "-o", "--output":
|
||||
skipNext = true
|
||||
outputFilename = params[i+1]
|
||||
case "--tempdir":
|
||||
skipNext = true
|
||||
tempdir = params[i+1]
|
||||
case "-T", "--test":
|
||||
runTests = true
|
||||
case "--enable-cgo":
|
||||
@@ -506,13 +376,13 @@ func main() {
|
||||
enablePIE = true
|
||||
case "--goos":
|
||||
skipNext = true
|
||||
buildEnv["GOOS"] = params[i+1]
|
||||
env["GOOS"] = params[i+1]
|
||||
case "--goarch":
|
||||
skipNext = true
|
||||
buildEnv["GOARCH"] = params[i+1]
|
||||
env["GOARCH"] = params[i+1]
|
||||
case "--goarm":
|
||||
skipNext = true
|
||||
buildEnv["GOARM"] = params[i+1]
|
||||
env["GOARM"] = params[i+1]
|
||||
case "-h":
|
||||
showUsage(os.Stdout)
|
||||
return
|
||||
@@ -525,8 +395,12 @@ func main() {
|
||||
|
||||
verbosePrintf("detected Go version %v\n", goVersion)
|
||||
|
||||
preserveSymbols := false
|
||||
for i := range buildTags {
|
||||
buildTags[i] = strings.TrimSpace(buildTags[i])
|
||||
if buildTags[i] == "debug" || buildTags[i] == "profile" {
|
||||
preserveSymbols = true
|
||||
}
|
||||
}
|
||||
|
||||
verbosePrintf("build tags: %s\n", buildTags)
|
||||
@@ -538,7 +412,7 @@ func main() {
|
||||
|
||||
if outputFilename == "" {
|
||||
outputFilename = config.Name
|
||||
if buildEnv["GOOS"] == "windows" {
|
||||
if env["GOOS"] == "windows" {
|
||||
outputFilename += ".exe"
|
||||
}
|
||||
}
|
||||
@@ -553,7 +427,11 @@ func main() {
|
||||
if version != "" {
|
||||
constants["main.version"] = version
|
||||
}
|
||||
ldflags := "-s -w " + constants.LDFlags()
|
||||
ldflags := constants.LDFlags()
|
||||
if !preserveSymbols {
|
||||
// Strip debug symbols.
|
||||
ldflags = "-s -w " + ldflags
|
||||
}
|
||||
verbosePrintf("ldflags: %s\n", ldflags)
|
||||
|
||||
var (
|
||||
@@ -567,57 +445,18 @@ func main() {
|
||||
}
|
||||
|
||||
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
|
||||
buildCWD, err := os.Getwd()
|
||||
if err != nil {
|
||||
die("unable to determine current working directory: %v\n", err)
|
||||
}
|
||||
|
||||
verbosePrintf("environment:\n go: %v\n build: %v\n", goEnv, buildEnv)
|
||||
|
||||
buildArgs = append(buildArgs,
|
||||
"-tags", strings.Join(buildTags, " "),
|
||||
"-ldflags", ldflags,
|
||||
"-o", output, buildTarget,
|
||||
)
|
||||
|
||||
err = build(buildCWD, buildEnv, buildArgs...)
|
||||
err = build(buildCWD, env, buildArgs...)
|
||||
if err != nil {
|
||||
die("build failed: %v\n", err)
|
||||
}
|
||||
@@ -627,7 +466,7 @@ func main() {
|
||||
|
||||
testArgs = append(testArgs, config.Tests...)
|
||||
|
||||
err = test(buildCWD, goEnv, testArgs...)
|
||||
err = test(buildCWD, env, testArgs...)
|
||||
if err != nil {
|
||||
die("running tests failed: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ Details
|
||||
{{ range $entry := .Entries }}{{ with $entry }}
|
||||
* {{ .Type }} #{{ .PrimaryID }}: {{ .Title }}
|
||||
{{ range $par := .Paragraphs }}
|
||||
{{ wrap $par 80 3 }}
|
||||
{{ wrapIndent $par 80 3 }}
|
||||
{{ end -}}
|
||||
{{ range $url := .IssueURLs }}
|
||||
{{ $url -}}
|
||||
|
||||
@@ -9,4 +9,4 @@ in case there aren't any issue links) is used as the primary ID.
|
||||
|
||||
https://github.com/restic/restic/issues/1234
|
||||
https://github.com/restic/restic/pull/55555
|
||||
https://forum.restic/.net/foo/bar/baz
|
||||
https://forum.restic.net/foo/bar/baz
|
||||
|
||||
0
changelog/unreleased/.gitkeep
Normal file
0
changelog/unreleased/.gitkeep
Normal file
9
changelog/unreleased/issue-1570
Normal file
9
changelog/unreleased/issue-1570
Normal file
@@ -0,0 +1,9 @@
|
||||
Enhancement: Support specifying multiple host flags for various commands
|
||||
|
||||
Previously commands didn't take more than one `--host` or `-H` argument into account, which could be limiting with e.g.
|
||||
the `forget` command.
|
||||
|
||||
The `dump`, `find`, `forget`, `ls`, `mount`, `restore`, `snapshots`, `stats` and `tag` commands will now take into account
|
||||
multiple `--host` and `-H` flags.
|
||||
|
||||
https://github.com/restic/restic/issues/1570
|
||||
5
changelog/unreleased/issue-2072
Normal file
5
changelog/unreleased/issue-2072
Normal file
@@ -0,0 +1,5 @@
|
||||
Enhancement: Display snapshot date when using `restic find`
|
||||
|
||||
Added the respective snapshot date to the output of `restic find`.
|
||||
|
||||
https://github.com/restic/restic/issues/2072
|
||||
5
changelog/unreleased/issue-2277
Normal file
5
changelog/unreleased/issue-2277
Normal file
@@ -0,0 +1,5 @@
|
||||
Enhancement: Add support for ppc64le
|
||||
|
||||
Adds support for ppc64le, the processor architecture from IBM.
|
||||
|
||||
https://github.com/restic/restic/issues/2277
|
||||
7
changelog/unreleased/issue-2281
Normal file
7
changelog/unreleased/issue-2281
Normal file
@@ -0,0 +1,7 @@
|
||||
Bugfix: Handle format verbs like '%' properly in `find` output
|
||||
|
||||
The JSON or "normal" output of the `find` command can now deal with file names
|
||||
that contain substrings which the Golang `fmt` package considers "format verbs"
|
||||
like `%s`.
|
||||
|
||||
https://github.com/restic/restic/issues/2281
|
||||
8
changelog/unreleased/issue-2298
Normal file
8
changelog/unreleased/issue-2298
Normal file
@@ -0,0 +1,8 @@
|
||||
Bugfix: Do not hang when run as a background job
|
||||
|
||||
Restic did hang on exit while restoring the terminal configuration when it was
|
||||
started as a background job, for example using `restic ... &`. This has been
|
||||
fixed by only restoring the terminal configuration when restic is interrupted
|
||||
while reading a password from the terminal.
|
||||
|
||||
https://github.com/restic/restic/issues/2298
|
||||
8
changelog/unreleased/issue-2389
Normal file
8
changelog/unreleased/issue-2389
Normal file
@@ -0,0 +1,8 @@
|
||||
Bugfix: Fix mangled json output of backup command
|
||||
|
||||
We've fixed a race condition in the json output of the backup command
|
||||
that could cause multiple lines to get mixed up. We've also ensured that
|
||||
the backup summary is printed last.
|
||||
|
||||
https://github.com/restic/restic/issues/2389
|
||||
https://github.com/restic/restic/pull/2545
|
||||
5
changelog/unreleased/issue-2390
Normal file
5
changelog/unreleased/issue-2390
Normal file
@@ -0,0 +1,5 @@
|
||||
Bugfix: Refresh lock timestamp
|
||||
|
||||
Long-running operations did not refresh lock timestamp, resulting in locks becoming stale. This is now fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/2390
|
||||
6
changelog/unreleased/issue-2429
Normal file
6
changelog/unreleased/issue-2429
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: backup --json reports total_bytes_processed as 0
|
||||
|
||||
We've fixed the json output of total_bytes_processed. The non-json output
|
||||
was already fixed with pull request #2138 but left the json output untouched.
|
||||
|
||||
https://github.com/restic/restic/issues/2429
|
||||
5
changelog/unreleased/issue-2469
Normal file
5
changelog/unreleased/issue-2469
Normal file
@@ -0,0 +1,5 @@
|
||||
Bugfix: Fix incorrect bytes stats in `diff` command
|
||||
|
||||
In some cases, the wrong number of bytes (e.g. 16777215.998 TiB) were reported by the `diff` command. This is now fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/2469
|
||||
9
changelog/unreleased/issue-2482
Normal file
9
changelog/unreleased/issue-2482
Normal file
@@ -0,0 +1,9 @@
|
||||
Change: Remove vendored dependencies
|
||||
|
||||
We've removed the vendored dependencies (in the subdir `vendor/`). When
|
||||
building restic, the Go compiler automatically fetches the dependencies. It
|
||||
will also cryptographically verify that the correct code has been fetched by
|
||||
using the hashes in `go.sum` (see the link to the documentation below).
|
||||
|
||||
https://github.com/restic/restic/issues/2482
|
||||
https://golang.org/cmd/go/#hdr-Module_downloading_and_verification
|
||||
16
changelog/unreleased/issue-2518
Normal file
16
changelog/unreleased/issue-2518
Normal file
@@ -0,0 +1,16 @@
|
||||
Bugfix: Do not crash with Synology NAS sftp server
|
||||
|
||||
It was found that when restic is used to store data on an sftp server on a
|
||||
Synology NAS with a relative path (one which does not start with a slash), it
|
||||
may go into an endless loop trying to create directories on the server. We've
|
||||
fixed this bug by using a function in the sftp library instead of our own
|
||||
implementation.
|
||||
|
||||
The bug was discovered because the Synology sftp server behaves erratic with
|
||||
non-absolute path (e.g. `home/restic-repo`). This can be resolved by just using
|
||||
an absolute path instead (`/home/restic-repo`). We've also added a paragraph in
|
||||
the FAQ.
|
||||
|
||||
https://github.com/restic/restic/issues/2518
|
||||
https://github.com/restic/restic/issues/2363
|
||||
https://github.com/restic/restic/pull/2530
|
||||
5
changelog/unreleased/issue-2531
Normal file
5
changelog/unreleased/issue-2531
Normal file
@@ -0,0 +1,5 @@
|
||||
Bugfix: Fix incorrect size calculation in `stats --mode restore-size`
|
||||
|
||||
The restore-size mode of stats was counting hard-linked files as if they were independent.
|
||||
|
||||
https://github.com/restic/restic/issues/2531
|
||||
5
changelog/unreleased/issue-2537
Normal file
5
changelog/unreleased/issue-2537
Normal file
@@ -0,0 +1,5 @@
|
||||
Bugfix: Fix incorrect file counts in `stats --mode restore-size`
|
||||
|
||||
The restore-size mode of stats was failing to count empty directories and some files with hard links.
|
||||
|
||||
https://github.com/restic/restic/issues/2537
|
||||
17
changelog/unreleased/pull-2195
Normal file
17
changelog/unreleased/pull-2195
Normal file
@@ -0,0 +1,17 @@
|
||||
Enhancement: Simplify and improve restore performance
|
||||
|
||||
Significantly improves restore performance of large files (i.e. 50M+):
|
||||
https://github.com/restic/restic/issues/2074
|
||||
https://forum.restic.net/t/restore-using-rclone-gdrive-backend-is-slow/1112/8
|
||||
https://forum.restic.net/t/degraded-restore-performance-s3-backend/1400
|
||||
|
||||
Fixes "not enough cache capacity" error during restore:
|
||||
https://github.com/restic/restic/issues/2244
|
||||
|
||||
NOTE: This new implementation does not guarantee order in which blobs
|
||||
are written to the target files and, for example, the last blob of a
|
||||
file can be written to the file before any of the preceeding file blobs.
|
||||
It is therefore possible to have gaps in the data written to the target
|
||||
files if restore fails or interrupted by the user.
|
||||
|
||||
https://github.com/restic/restic/pull/2195
|
||||
5
changelog/unreleased/pull-2423
Normal file
5
changelog/unreleased/pull-2423
Normal file
@@ -0,0 +1,5 @@
|
||||
Enhancement: support user@domain parsing as user
|
||||
|
||||
Added the ability for user@domain-like users to be authenticated over SFTP servers.
|
||||
|
||||
https://github.com/restic/restic/pull/2423
|
||||
7
changelog/unreleased/pull-2576
Normal file
7
changelog/unreleased/pull-2576
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Improve the chunking algorithm
|
||||
|
||||
We've updated the chunker library responsible for splitting files into smaller
|
||||
blocks. It should improve the chunking throughput by 5-10% depending on the
|
||||
CPU.
|
||||
|
||||
https://github.com/restic/restic/pull/2576
|
||||
6
changelog/unreleased/pull-2592
Normal file
6
changelog/unreleased/pull-2592
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: SFTP backend supports IPv6 addresses
|
||||
|
||||
The SFTP backend now supports IPv6 addresses natively, without relying on
|
||||
aliases in the external SSH configuration.
|
||||
|
||||
https://github.com/restic/restic/pull/2592
|
||||
6
changelog/unreleased/pull-2600
Normal file
6
changelog/unreleased/pull-2600
Normal file
@@ -0,0 +1,6 @@
|
||||
Change: Require Go >= 1.11
|
||||
|
||||
Restic now requires Go to be at least 1.11. This allows simplifications in the
|
||||
build process and removing workarounds.
|
||||
|
||||
https://github.com/restic/restic/pull/2600
|
||||
7
changelog/unreleased/pull-2607
Normal file
7
changelog/unreleased/pull-2607
Normal file
@@ -0,0 +1,7 @@
|
||||
Bugfix: Honor RESTIC_CACHE_DIR environment variable on Mac and Windows
|
||||
|
||||
On Mac and Windows, the RESTIC_CACHE_DIR environment variable was ignored.
|
||||
This variable can now be used on all platforms to set the directory where
|
||||
restic stores caches.
|
||||
|
||||
https://github.com/restic/restic/pull/2607
|
||||
6
changelog/unreleased/pull-2668
Normal file
6
changelog/unreleased/pull-2668
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Don't abort the stats command when data blobs are missing
|
||||
|
||||
Runing the stats command in the blobs-per-file mode on a repository with
|
||||
missing data blobs previously resulted in a crash.
|
||||
|
||||
https://github.com/restic/restic/pull/2668
|
||||
@@ -1,18 +1,20 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.6.4
|
||||
- 1.7.4
|
||||
- tip
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
go: "1.9.x"
|
||||
- os: linux
|
||||
go: "1.10.x"
|
||||
- os: linux
|
||||
go: "tip"
|
||||
- os: osx
|
||||
go: "1.10.x"
|
||||
|
||||
install:
|
||||
- go get -t ./...
|
||||
- go get -u github.com/golang/lint/golint
|
||||
- go get -u golang.org/x/lint/golint
|
||||
- go get -u golang.org/x/tools/cmd/goimports
|
||||
|
||||
script:
|
||||
@@ -1,5 +1,5 @@
|
||||
[](http://godoc.org/github.com/restic/chunker)
|
||||
[](https://travis-ci.org/restic/chunker)
|
||||
[](https://travis-ci.com/restic/chunker)
|
||||
|
||||
The package `chunker` implements content-defined-chunking (CDC) based on a
|
||||
rolling Rabin Hash. The library is part of the [restic backup
|
||||
@@ -2,6 +2,7 @@ package chunker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
@@ -47,7 +48,7 @@ type Chunk struct {
|
||||
|
||||
type chunkerState struct {
|
||||
window [windowSize]byte
|
||||
wpos int
|
||||
wpos uint
|
||||
|
||||
buf []byte
|
||||
bpos uint
|
||||
@@ -225,6 +226,12 @@ func (c *Chunker) Next(data []byte) (Chunk, error) {
|
||||
tabout := c.tables.out
|
||||
tabmod := c.tables.mod
|
||||
polShift := c.polShift
|
||||
// go guarantees the expected behavior for bit shifts even for shift counts
|
||||
// larger than the value width. Bounding the value of polShift allows the compiler
|
||||
// to optimize the code for 'digest >> polShift'
|
||||
if polShift > 53-8 {
|
||||
return Chunk{}, errors.New("the polynomial must have a degree less than or equal 53")
|
||||
}
|
||||
minSize := c.MinSize
|
||||
maxSize := c.MaxSize
|
||||
buf := c.buf
|
||||
@@ -259,6 +266,10 @@ func (c *Chunker) Next(data []byte) (Chunk, error) {
|
||||
return Chunk{}, err
|
||||
}
|
||||
|
||||
if n < 0 {
|
||||
return Chunk{}, fmt.Errorf("ReadFull returned negative number of bytes read: %v", n)
|
||||
}
|
||||
|
||||
c.bpos = 0
|
||||
c.bmax = uint(n)
|
||||
}
|
||||
@@ -291,10 +302,12 @@ func (c *Chunker) Next(data []byte) (Chunk, error) {
|
||||
wpos := c.wpos
|
||||
for _, b := range buf[c.bpos:c.bmax] {
|
||||
// slide(b)
|
||||
// limit wpos before to elide array bound checks
|
||||
wpos = wpos % windowSize
|
||||
out := win[wpos]
|
||||
win[wpos] = b
|
||||
digest ^= uint64(tabout[out])
|
||||
wpos = (wpos + 1) % windowSize
|
||||
wpos++
|
||||
|
||||
// updateDigest
|
||||
index := byte(digest >> polShift)
|
||||
@@ -331,7 +344,7 @@ func (c *Chunker) Next(data []byte) (Chunk, error) {
|
||||
}
|
||||
c.digest = digest
|
||||
c.window = win
|
||||
c.wpos = wpos
|
||||
c.wpos = wpos % windowSize
|
||||
|
||||
steps := c.bmax - c.bpos
|
||||
if steps > 0 {
|
||||
347
chunker/chunker_test.go
Normal file
347
chunker/chunker_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package chunker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func parseDigest(s string) []byte {
|
||||
d, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
type chunk struct {
|
||||
Length uint
|
||||
CutFP uint64
|
||||
Digest []byte
|
||||
}
|
||||
|
||||
// polynomial used for all the tests below
|
||||
const testPol = Pol(0x3DA3358B4DC173)
|
||||
|
||||
// created for 32MB of random data out of math/rand's Uint32() seeded by
|
||||
// constant 23
|
||||
//
|
||||
// chunking configuration:
|
||||
// window size 64, avg chunksize 1<<20, min chunksize 1<<19, max chunksize 1<<23
|
||||
// polynom 0x3DA3358B4DC173
|
||||
var chunks1 = []chunk{
|
||||
chunk{2163460, 0x000b98d4cdf00000, parseDigest("4b94cb2cf293855ea43bf766731c74969b91aa6bf3c078719aabdd19860d590d")},
|
||||
chunk{643703, 0x000d4e8364d00000, parseDigest("5727a63c0964f365ab8ed2ccf604912f2ea7be29759a2b53ede4d6841e397407")},
|
||||
chunk{1528956, 0x0015a25c2ef00000, parseDigest("a73759636a1e7a2758767791c69e81b69fb49236c6929e5d1b654e06e37674ba")},
|
||||
chunk{1955808, 0x00102a8242e00000, parseDigest("c955fb059409b25f07e5ae09defbbc2aadf117c97a3724e06ad4abd2787e6824")},
|
||||
chunk{2222372, 0x00045da878000000, parseDigest("6ba5e9f7e1b310722be3627716cf469be941f7f3e39a4c3bcefea492ec31ee56")},
|
||||
chunk{2538687, 0x00198a8179900000, parseDigest("8687937412f654b5cfe4a82b08f28393a0c040f77c6f95e26742c2fc4254bfde")},
|
||||
chunk{609606, 0x001d4e8d17100000, parseDigest("5da820742ff5feb3369112938d3095785487456f65a8efc4b96dac4be7ebb259")},
|
||||
chunk{1205738, 0x000a7204dd600000, parseDigest("cc70d8fad5472beb031b1aca356bcab86c7368f40faa24fe5f8922c6c268c299")},
|
||||
chunk{959742, 0x00183e71e1400000, parseDigest("4065bdd778f95676c92b38ac265d361f81bff17d76e5d9452cf985a2ea5a4e39")},
|
||||
chunk{4036109, 0x001fec043c700000, parseDigest("b9cf166e75200eb4993fc9b6e22300a6790c75e6b0fc8f3f29b68a752d42f275")},
|
||||
chunk{1525894, 0x000b1574b1500000, parseDigest("2f238180e4ca1f7520a05f3d6059233926341090f9236ce677690c1823eccab3")},
|
||||
chunk{1352720, 0x00018965f2e00000, parseDigest("afd12f13286a3901430de816e62b85cc62468c059295ce5888b76b3af9028d84")},
|
||||
chunk{811884, 0x00155628aa100000, parseDigest("42d0cdb1ee7c48e552705d18e061abb70ae7957027db8ae8db37ec756472a70a")},
|
||||
chunk{1282314, 0x001909a0a1400000, parseDigest("819721c2457426eb4f4c7565050c44c32076a56fa9b4515a1c7796441730eb58")},
|
||||
chunk{1318021, 0x001cceb980000000, parseDigest("842eb53543db55bacac5e25cb91e43cc2e310fe5f9acc1aee86bdf5e91389374")},
|
||||
chunk{948640, 0x0011f7a470a00000, parseDigest("b8e36bf7019bb96ac3fb7867659d2167d9d3b3148c09fe0de45850b8fe577185")},
|
||||
chunk{645464, 0x00030ce2d9400000, parseDigest("5584bd27982191c3329f01ed846bfd266e96548dfa87018f745c33cfc240211d")},
|
||||
chunk{533758, 0x0004435c53c00000, parseDigest("4da778a25b72a9a0d53529eccfe2e5865a789116cb1800f470d8df685a8ab05d")},
|
||||
chunk{1128303, 0x0000c48517800000, parseDigest("08c6b0b38095b348d80300f0be4c5184d2744a17147c2cba5cc4315abf4c048f")},
|
||||
chunk{800374, 0x000968473f900000, parseDigest("820284d2c8fd243429674c996d8eb8d3450cbc32421f43113e980f516282c7bf")},
|
||||
chunk{2453512, 0x001e197c92600000, parseDigest("5fa870ed107c67704258e5e50abe67509fb73562caf77caa843b5f243425d853")},
|
||||
chunk{2651975, 0x000ae6c868000000, parseDigest("181347d2bbec32bef77ad5e9001e6af80f6abcf3576549384d334ee00c1988d8")},
|
||||
chunk{237392, 0x0000000000000001, parseDigest("fcd567f5d866357a8e299fd5b2359bb2c8157c30395229c4e9b0a353944a7978")},
|
||||
}
|
||||
|
||||
// test if nullbytes are correctly split, even if length is a multiple of MinSize.
|
||||
var chunks2 = []chunk{
|
||||
chunk{MinSize, 0, parseDigest("07854d2fef297a06ba81685e660c332de36d5d18d546927d30daad6d7fda1541")},
|
||||
chunk{MinSize, 0, parseDigest("07854d2fef297a06ba81685e660c332de36d5d18d546927d30daad6d7fda1541")},
|
||||
chunk{MinSize, 0, parseDigest("07854d2fef297a06ba81685e660c332de36d5d18d546927d30daad6d7fda1541")},
|
||||
chunk{MinSize, 0, parseDigest("07854d2fef297a06ba81685e660c332de36d5d18d546927d30daad6d7fda1541")},
|
||||
}
|
||||
|
||||
// the same as chunks1, but avg chunksize is 1<<19
|
||||
var chunks3 = []chunk{
|
||||
chunk{1491586, 0x00023e586ea80000, parseDigest("4c008237df602048039287427171cef568a6cb965d1b5ca28dc80504a24bb061")},
|
||||
chunk{671874, 0x000b98d4cdf00000, parseDigest("fa8a42321b90c3d4ce9dd850562b2fd0c0fe4bdd26cf01a24f22046a224225d3")},
|
||||
chunk{643703, 0x000d4e8364d00000, parseDigest("5727a63c0964f365ab8ed2ccf604912f2ea7be29759a2b53ede4d6841e397407")},
|
||||
chunk{1284146, 0x0012b527e4780000, parseDigest("16d04cafecbeae9eaedd49da14c7ad7cdc2b1cc8569e5c16c32c9fb045aa899a")},
|
||||
chunk{823366, 0x000d1d6752180000, parseDigest("48662c118514817825ad4761e8e2e5f28f9bd8281b07e95dcafc6d02e0aa45c3")},
|
||||
chunk{810134, 0x0016071b6e180000, parseDigest("f629581aa05562f97f2c359890734c8574c5575da32f9289c5ba70bfd05f3f46")},
|
||||
chunk{567118, 0x00102a8242e00000, parseDigest("d4f0797c56c60d01bac33bfd49957a4816b6c067fc155b026de8a214cab4d70a")},
|
||||
chunk{821315, 0x001b3e42c8180000, parseDigest("8ebd0fd5db0293bd19140da936eb8b1bbd3cd6ffbec487385b956790014751ca")},
|
||||
chunk{1401057, 0x00045da878000000, parseDigest("001360af59adf4871ef138cfa2bb49007e86edaf5ac2d6f0b3d3014510991848")},
|
||||
chunk{2311122, 0x0005cbd885380000, parseDigest("8276d489b566086d9da95dc5c5fe6fc7d72646dd3308ced6b5b6ddb8595f0aa1")},
|
||||
chunk{608723, 0x001cfcd86f280000, parseDigest("518db33ba6a79d4f3720946f3785c05b9611082586d47ea58390fc2f6de9449e")},
|
||||
chunk{980456, 0x0013edb7a7f80000, parseDigest("0121b1690738395e15fecba1410cd0bf13fde02225160cad148829f77e7b6c99")},
|
||||
chunk{1140278, 0x0001f9f017e80000, parseDigest("28ca7c74804b5075d4f5eeb11f0845d99f62e8ea3a42b9a05c7bd5f2fca619dd")},
|
||||
chunk{2015542, 0x00097bf5d8180000, parseDigest("6fe8291f427d48650a5f0f944305d3a2dbc649bd401d2655fc0bdd42e890ca5a")},
|
||||
chunk{904752, 0x000e1863eff80000, parseDigest("62af1f1eb3f588d18aff28473303cc4731fc3cafcc52ce818fee3c4c2820854d")},
|
||||
chunk{713072, 0x001f3bb1b9b80000, parseDigest("4bda9dc2e3031d004d87a5cc93fe5207c4b0843186481b8f31597dc6ffa1496c")},
|
||||
chunk{675937, 0x001fec043c700000, parseDigest("5299c8c5acec1b90bb020cd75718aab5e12abb9bf66291465fd10e6a823a8b4a")},
|
||||
chunk{1525894, 0x000b1574b1500000, parseDigest("2f238180e4ca1f7520a05f3d6059233926341090f9236ce677690c1823eccab3")},
|
||||
chunk{1352720, 0x00018965f2e00000, parseDigest("afd12f13286a3901430de816e62b85cc62468c059295ce5888b76b3af9028d84")},
|
||||
chunk{811884, 0x00155628aa100000, parseDigest("42d0cdb1ee7c48e552705d18e061abb70ae7957027db8ae8db37ec756472a70a")},
|
||||
chunk{1282314, 0x001909a0a1400000, parseDigest("819721c2457426eb4f4c7565050c44c32076a56fa9b4515a1c7796441730eb58")},
|
||||
chunk{1093738, 0x0017f5d048880000, parseDigest("5dddfa7a241b68f65d267744bdb082ee865f3c2f0d8b946ea0ee47868a01bbff")},
|
||||
chunk{962003, 0x000b921f7ef80000, parseDigest("0cb5c9ebba196b441c715c8d805f6e7143a81cd5b0d2c65c6aacf59ca9124af9")},
|
||||
chunk{856384, 0x00030ce2d9400000, parseDigest("7734b206d46f3f387e8661e81edf5b1a91ea681867beb5831c18aaa86632d7fb")},
|
||||
chunk{533758, 0x0004435c53c00000, parseDigest("4da778a25b72a9a0d53529eccfe2e5865a789116cb1800f470d8df685a8ab05d")},
|
||||
chunk{1128303, 0x0000c48517800000, parseDigest("08c6b0b38095b348d80300f0be4c5184d2744a17147c2cba5cc4315abf4c048f")},
|
||||
chunk{800374, 0x000968473f900000, parseDigest("820284d2c8fd243429674c996d8eb8d3450cbc32421f43113e980f516282c7bf")},
|
||||
chunk{2453512, 0x001e197c92600000, parseDigest("5fa870ed107c67704258e5e50abe67509fb73562caf77caa843b5f243425d853")},
|
||||
chunk{665901, 0x00118c842cb80000, parseDigest("deceec26163842fdef6560311c69bf8a9871a56e16d719e2c4b7e4d668ceb61f")},
|
||||
chunk{1986074, 0x000ae6c868000000, parseDigest("64cd64bf3c3bc389eb20df8310f0427d1c36ab2eaaf09e346bfa7f0453fc1a18")},
|
||||
chunk{237392, 0x0000000000000001, parseDigest("fcd567f5d866357a8e299fd5b2359bb2c8157c30395229c4e9b0a353944a7978")},
|
||||
}
|
||||
|
||||
func testWithData(t *testing.T, chnker *Chunker, testChunks []chunk, checkDigest bool) []Chunk {
|
||||
chunks := []Chunk{}
|
||||
|
||||
pos := uint(0)
|
||||
for i, chunk := range testChunks {
|
||||
c, err := chnker.Next(nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Error returned with chunk %d: %v", i, err)
|
||||
}
|
||||
|
||||
if c.Start != pos {
|
||||
t.Fatalf("Start for chunk %d does not match: expected %d, got %d",
|
||||
i, pos, c.Start)
|
||||
}
|
||||
|
||||
if c.Length != chunk.Length {
|
||||
t.Fatalf("Length for chunk %d does not match: expected %d, got %d",
|
||||
i, chunk.Length, c.Length)
|
||||
}
|
||||
|
||||
if c.Cut != chunk.CutFP {
|
||||
t.Fatalf("Cut fingerprint for chunk %d/%d does not match: expected %016x, got %016x",
|
||||
i, len(chunks)-1, chunk.CutFP, c.Cut)
|
||||
}
|
||||
|
||||
if checkDigest {
|
||||
digest := hashData(c.Data)
|
||||
if !bytes.Equal(chunk.Digest, digest) {
|
||||
t.Fatalf("Digest fingerprint for chunk %d/%d does not match: expected %02x, got %02x",
|
||||
i, len(chunks)-1, chunk.Digest, digest)
|
||||
}
|
||||
}
|
||||
|
||||
pos += c.Length
|
||||
chunks = append(chunks, c)
|
||||
}
|
||||
|
||||
_, err := chnker.Next(nil)
|
||||
if err != io.EOF {
|
||||
t.Fatal("Wrong error returned after last chunk")
|
||||
}
|
||||
|
||||
if len(chunks) != len(testChunks) {
|
||||
t.Fatal("Amounts of test and resulting chunks do not match")
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
func getRandom(seed int64, count int) []byte {
|
||||
buf := make([]byte, count)
|
||||
|
||||
rnd := rand.New(rand.NewSource(seed))
|
||||
for i := 0; i < count; i += 4 {
|
||||
r := rnd.Uint32()
|
||||
buf[i] = byte(r)
|
||||
buf[i+1] = byte(r >> 8)
|
||||
buf[i+2] = byte(r >> 16)
|
||||
buf[i+3] = byte(r >> 24)
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func hashData(d []byte) []byte {
|
||||
h := sha256.New()
|
||||
h.Write(d)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func TestChunker(t *testing.T) {
|
||||
// setup data source
|
||||
buf := getRandom(23, 32*1024*1024)
|
||||
ch := New(bytes.NewReader(buf), testPol)
|
||||
testWithData(t, ch, chunks1, true)
|
||||
|
||||
// setup nullbyte data source
|
||||
buf = bytes.Repeat([]byte{0}, len(chunks2)*MinSize)
|
||||
ch = New(bytes.NewReader(buf), testPol)
|
||||
|
||||
testWithData(t, ch, chunks2, true)
|
||||
}
|
||||
|
||||
func TestChunkerWithCustomAverageBits(t *testing.T) {
|
||||
buf := getRandom(23, 32*1024*1024)
|
||||
ch := New(bytes.NewReader(buf), testPol)
|
||||
|
||||
// sligthly decrease averageBits to get more chunks
|
||||
ch.SetAverageBits(19)
|
||||
testWithData(t, ch, chunks3, true)
|
||||
}
|
||||
|
||||
func TestChunkerReset(t *testing.T) {
|
||||
buf := getRandom(23, 32*1024*1024)
|
||||
ch := New(bytes.NewReader(buf), testPol)
|
||||
testWithData(t, ch, chunks1, true)
|
||||
|
||||
ch.Reset(bytes.NewReader(buf), testPol)
|
||||
testWithData(t, ch, chunks1, true)
|
||||
}
|
||||
|
||||
func TestChunkerWithRandomPolynomial(t *testing.T) {
|
||||
// setup data source
|
||||
buf := getRandom(23, 32*1024*1024)
|
||||
|
||||
// generate a new random polynomial
|
||||
start := time.Now()
|
||||
p, err := RandomPolynomial()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("generating random polynomial took %v", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
ch := New(bytes.NewReader(buf), p)
|
||||
t.Logf("creating chunker took %v", time.Since(start))
|
||||
|
||||
// make sure that first chunk is different
|
||||
c, err := ch.Next(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if c.Cut == chunks1[0].CutFP {
|
||||
t.Fatal("Cut point is the same")
|
||||
}
|
||||
|
||||
if c.Length == chunks1[0].Length {
|
||||
t.Fatal("Length is the same")
|
||||
}
|
||||
|
||||
if bytes.Equal(hashData(c.Data), chunks1[0].Digest) {
|
||||
t.Fatal("Digest is the same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkerWithoutHash(t *testing.T) {
|
||||
// setup data source
|
||||
buf := getRandom(23, 32*1024*1024)
|
||||
|
||||
ch := New(bytes.NewReader(buf), testPol)
|
||||
chunks := testWithData(t, ch, chunks1, false)
|
||||
|
||||
// test reader
|
||||
for i, c := range chunks {
|
||||
if uint(len(c.Data)) != chunks1[i].Length {
|
||||
t.Fatalf("reader returned wrong number of bytes: expected %d, got %d",
|
||||
chunks1[i].Length, len(c.Data))
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[c.Start:c.Start+c.Length], c.Data) {
|
||||
t.Fatalf("invalid data for chunk returned: expected %02x, got %02x",
|
||||
buf[c.Start:c.Start+c.Length], c.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// setup nullbyte data source
|
||||
buf = bytes.Repeat([]byte{0}, len(chunks2)*MinSize)
|
||||
ch = New(bytes.NewReader(buf), testPol)
|
||||
|
||||
testWithData(t, ch, chunks2, false)
|
||||
}
|
||||
|
||||
func benchmarkChunker(b *testing.B, checkDigest bool) {
|
||||
size := 32 * 1024 * 1024
|
||||
rd := bytes.NewReader(getRandom(23, size))
|
||||
ch := New(rd, testPol)
|
||||
buf := make([]byte, MaxSize)
|
||||
|
||||
b.ResetTimer()
|
||||
b.SetBytes(int64(size))
|
||||
|
||||
var chunks int
|
||||
for i := 0; i < b.N; i++ {
|
||||
chunks = 0
|
||||
|
||||
_, err := rd.Seek(0, 0)
|
||||
if err != nil {
|
||||
b.Fatalf("Seek() return error %v", err)
|
||||
}
|
||||
|
||||
ch.Reset(rd, testPol)
|
||||
|
||||
cur := 0
|
||||
for {
|
||||
chunk, err := ch.Next(buf)
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
b.Fatalf("Unexpected error occurred: %v", err)
|
||||
}
|
||||
|
||||
if chunk.Length != chunks1[cur].Length {
|
||||
b.Errorf("wrong chunk length, want %d, got %d",
|
||||
chunks1[cur].Length, chunk.Length)
|
||||
}
|
||||
|
||||
if chunk.Cut != chunks1[cur].CutFP {
|
||||
b.Errorf("wrong cut fingerprint, want 0x%x, got 0x%x",
|
||||
chunks1[cur].CutFP, chunk.Cut)
|
||||
}
|
||||
|
||||
if checkDigest {
|
||||
h := hashData(chunk.Data)
|
||||
if !bytes.Equal(h, chunks1[cur].Digest) {
|
||||
b.Errorf("wrong digest, want %x, got %x",
|
||||
chunks1[cur].Digest, h)
|
||||
}
|
||||
}
|
||||
|
||||
chunks++
|
||||
cur++
|
||||
}
|
||||
}
|
||||
|
||||
b.Logf("%d chunks, average chunk size: %d bytes", chunks, size/chunks)
|
||||
}
|
||||
|
||||
func BenchmarkChunkerWithSHA256(b *testing.B) {
|
||||
benchmarkChunker(b, true)
|
||||
}
|
||||
|
||||
func BenchmarkChunker(b *testing.B) {
|
||||
benchmarkChunker(b, false)
|
||||
}
|
||||
|
||||
func BenchmarkNewChunker(b *testing.B) {
|
||||
p, err := RandomPolynomial()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
New(bytes.NewBuffer(nil), p)
|
||||
}
|
||||
}
|
||||
39
chunker/example_test.go
Normal file
39
chunker/example_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package chunker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func ExampleChunker() {
|
||||
// generate 32MiB of deterministic pseudo-random data
|
||||
data := getRandom(23, 32*1024*1024)
|
||||
|
||||
// create a chunker
|
||||
chunker := New(bytes.NewReader(data), Pol(0x3DA3358B4DC173))
|
||||
|
||||
// reuse this buffer
|
||||
buf := make([]byte, 8*1024*1024)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
chunk, err := chunker.Next(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%d %02x\n", chunk.Length, sha256.Sum256(chunk.Data))
|
||||
}
|
||||
|
||||
// Output:
|
||||
// 2163460 4b94cb2cf293855ea43bf766731c74969b91aa6bf3c078719aabdd19860d590d
|
||||
// 643703 5727a63c0964f365ab8ed2ccf604912f2ea7be29759a2b53ede4d6841e397407
|
||||
// 1528956 a73759636a1e7a2758767791c69e81b69fb49236c6929e5d1b654e06e37674ba
|
||||
// 1955808 c955fb059409b25f07e5ae09defbbc2aadf117c97a3724e06ad4abd2787e6824
|
||||
// 2222372 6ba5e9f7e1b310722be3627716cf469be941f7f3e39a4c3bcefea492ec31ee56
|
||||
}
|
||||
3
chunker/go.mod
Normal file
3
chunker/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/restic/chunker
|
||||
|
||||
go 1.14
|
||||
@@ -94,7 +94,6 @@ func (x Pol) Deg() int {
|
||||
}
|
||||
|
||||
if uint64(x)&0x2 > 0 {
|
||||
x >>= 1
|
||||
r |= 1
|
||||
}
|
||||
|
||||
424
chunker/polynomials_test.go
Normal file
424
chunker/polynomials_test.go
Normal file
@@ -0,0 +1,424 @@
|
||||
package chunker
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var polAddTests = []struct {
|
||||
x, y Pol
|
||||
sum Pol
|
||||
}{
|
||||
{23, 16, 23 ^ 16},
|
||||
{0x9a7e30d1e855e0a0, 0x670102a1f4bcd414, 0xfd7f32701ce934b4},
|
||||
{0x9a7e30d1e855e0a0, 0x9a7e30d1e855e0a0, 0},
|
||||
}
|
||||
|
||||
func TestPolAdd(t *testing.T) {
|
||||
for i, test := range polAddTests {
|
||||
if test.sum != test.x.Add(test.y) {
|
||||
t.Errorf("test %d failed: sum != x+y", i)
|
||||
}
|
||||
|
||||
if test.sum != test.y.Add(test.x) {
|
||||
t.Errorf("test %d failed: sum != y+x", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseBin(s string) Pol {
|
||||
i, err := strconv.ParseUint(s, 2, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return Pol(i)
|
||||
}
|
||||
|
||||
var polMulTests = []struct {
|
||||
x, y Pol
|
||||
res Pol
|
||||
}{
|
||||
{1, 2, 2},
|
||||
{
|
||||
parseBin("1101"),
|
||||
parseBin("10"),
|
||||
parseBin("11010"),
|
||||
},
|
||||
{
|
||||
parseBin("1101"),
|
||||
parseBin("11"),
|
||||
parseBin("10111"),
|
||||
},
|
||||
{
|
||||
0x40000000,
|
||||
0x40000000,
|
||||
0x1000000000000000,
|
||||
},
|
||||
{
|
||||
parseBin("1010"),
|
||||
parseBin("100100"),
|
||||
parseBin("101101000"),
|
||||
},
|
||||
{
|
||||
parseBin("100"),
|
||||
parseBin("11"),
|
||||
parseBin("1100"),
|
||||
},
|
||||
{
|
||||
parseBin("11"),
|
||||
parseBin("110101"),
|
||||
parseBin("1011111"),
|
||||
},
|
||||
{
|
||||
parseBin("10011"),
|
||||
parseBin("110101"),
|
||||
parseBin("1100001111"),
|
||||
},
|
||||
}
|
||||
|
||||
func TestPolMul(t *testing.T) {
|
||||
for i, test := range polMulTests {
|
||||
m := test.x.Mul(test.y)
|
||||
if test.res != m {
|
||||
t.Errorf("TestPolMul failed for test %d: %v * %v: want %v, got %v",
|
||||
i, test.x, test.y, test.res, m)
|
||||
}
|
||||
m = test.y.Mul(test.x)
|
||||
if test.res != test.y.Mul(test.x) {
|
||||
t.Errorf("TestPolMul failed for %d: %v * %v: want %v, got %v",
|
||||
i, test.x, test.y, test.res, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolMulOverflow(t *testing.T) {
|
||||
defer func() {
|
||||
// try to recover overflow error
|
||||
err := recover()
|
||||
|
||||
if e, ok := err.(string); ok && e == "multiplication would overflow uint64" {
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("invalid error raised: %v", err)
|
||||
// re-raise error if not overflow
|
||||
panic(err)
|
||||
}()
|
||||
|
||||
x := Pol(1 << 63)
|
||||
x.Mul(2)
|
||||
t.Fatal("overflow test did not panic")
|
||||
}
|
||||
|
||||
var polDivTests = []struct {
|
||||
x, y Pol
|
||||
res Pol
|
||||
}{
|
||||
{10, 50, 0},
|
||||
{0, 1, 0},
|
||||
{
|
||||
parseBin("101101000"), // 0x168
|
||||
parseBin("1010"), // 0xa
|
||||
parseBin("100100"), // 0x24
|
||||
},
|
||||
{2, 2, 1},
|
||||
{
|
||||
0x8000000000000000,
|
||||
0x8000000000000000,
|
||||
1,
|
||||
},
|
||||
{
|
||||
parseBin("1100"),
|
||||
parseBin("100"),
|
||||
parseBin("11"),
|
||||
},
|
||||
{
|
||||
parseBin("1100001111"),
|
||||
parseBin("10011"),
|
||||
parseBin("110101"),
|
||||
},
|
||||
}
|
||||
|
||||
func TestPolDiv(t *testing.T) {
|
||||
for i, test := range polDivTests {
|
||||
m := test.x.Div(test.y)
|
||||
if test.res != m {
|
||||
t.Errorf("TestPolDiv failed for test %d: %v * %v: want %v, got %v",
|
||||
i, test.x, test.y, test.res, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolDeg(t *testing.T) {
|
||||
var x Pol
|
||||
if x.Deg() != -1 {
|
||||
t.Errorf("deg(0) is not -1: %v", x.Deg())
|
||||
}
|
||||
|
||||
x = 1
|
||||
if x.Deg() != 0 {
|
||||
t.Errorf("deg(1) is not 0: %v", x.Deg())
|
||||
}
|
||||
|
||||
for i := 0; i < 64; i++ {
|
||||
x = 1 << uint(i)
|
||||
if x.Deg() != i {
|
||||
t.Errorf("deg(1<<%d) is not %d: %v", i, i, x.Deg())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var polModTests = []struct {
|
||||
x, y Pol
|
||||
res Pol
|
||||
}{
|
||||
{10, 50, 10},
|
||||
{0, 1, 0},
|
||||
{
|
||||
parseBin("101101001"),
|
||||
parseBin("1010"),
|
||||
parseBin("1"),
|
||||
},
|
||||
{2, 2, 0},
|
||||
{
|
||||
0x8000000000000000,
|
||||
0x8000000000000000,
|
||||
0,
|
||||
},
|
||||
{
|
||||
parseBin("1100"),
|
||||
parseBin("100"),
|
||||
parseBin("0"),
|
||||
},
|
||||
{
|
||||
parseBin("1100001111"),
|
||||
parseBin("10011"),
|
||||
parseBin("0"),
|
||||
},
|
||||
}
|
||||
|
||||
func TestPolModt(t *testing.T) {
|
||||
for i, test := range polModTests {
|
||||
res := test.x.Mod(test.y)
|
||||
if test.res != res {
|
||||
t.Errorf("test %d failed: want %v, got %v", i, test.res, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPolDivMod(t *testing.B) {
|
||||
f := Pol(0x2482734cacca49)
|
||||
g := Pol(0x3af4b284899)
|
||||
|
||||
for i := 0; i < t.N; i++ {
|
||||
g.DivMod(f)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPolDiv(t *testing.B) {
|
||||
f := Pol(0x2482734cacca49)
|
||||
g := Pol(0x3af4b284899)
|
||||
|
||||
for i := 0; i < t.N; i++ {
|
||||
g.Div(f)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPolMod(t *testing.B) {
|
||||
f := Pol(0x2482734cacca49)
|
||||
g := Pol(0x3af4b284899)
|
||||
|
||||
for i := 0; i < t.N; i++ {
|
||||
g.Mod(f)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPolDeg(t *testing.B) {
|
||||
f := Pol(0x3af4b284899)
|
||||
d := f.Deg()
|
||||
if d != 41 {
|
||||
t.Fatalf("BenchmalPolDeg: Wrong degree %d returned, expected %d",
|
||||
d, 41)
|
||||
}
|
||||
|
||||
for i := 0; i < t.N; i++ {
|
||||
f.Deg()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomPolynomial(t *testing.T) {
|
||||
_, err := RandomPolynomial()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRandomPolynomial(t *testing.B) {
|
||||
for i := 0; i < t.N; i++ {
|
||||
_, err := RandomPolynomial()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandPolynomial(t *testing.T) {
|
||||
pol := Pol(0x3DA3358B4DC173)
|
||||
s := pol.Expand()
|
||||
if s != "x^53+x^52+x^51+x^50+x^48+x^47+x^45+x^41+x^40+x^37+x^36+x^34+x^32+x^31+x^27+x^25+x^24+x^22+x^19+x^18+x^16+x^15+x^14+x^8+x^6+x^5+x^4+x+1" {
|
||||
t.Fatal("wrong result")
|
||||
}
|
||||
}
|
||||
|
||||
var polIrredTests = []struct {
|
||||
f Pol
|
||||
irred bool
|
||||
}{
|
||||
{0x38f1e565e288df, false},
|
||||
{0x3DA3358B4DC173, true},
|
||||
{0x30a8295b9d5c91, false},
|
||||
{0x255f4350b962cb, false},
|
||||
{0x267f776110a235, false},
|
||||
{0x2f4dae10d41227, false},
|
||||
{0x2482734cacca49, true},
|
||||
{0x312daf4b284899, false},
|
||||
{0x29dfb6553d01d1, false},
|
||||
{0x3548245eb26257, false},
|
||||
{0x3199e7ef4211b3, false},
|
||||
{0x362f39017dae8b, false},
|
||||
{0x200d57aa6fdacb, false},
|
||||
{0x35e0a4efa1d275, false},
|
||||
{0x2ced55b026577f, false},
|
||||
{0x260b012010893d, false},
|
||||
{0x2df29cbcd59e9d, false},
|
||||
{0x3f2ac7488bd429, false},
|
||||
{0x3e5cb1711669fb, false},
|
||||
{0x226d8de57a9959, false},
|
||||
{0x3c8de80aaf5835, false},
|
||||
{0x2026a59efb219b, false},
|
||||
{0x39dfa4d13fb231, false},
|
||||
{0x3143d0464b3299, false},
|
||||
}
|
||||
|
||||
func TestPolIrreducible(t *testing.T) {
|
||||
for _, test := range polIrredTests {
|
||||
if test.f.Irreducible() != test.irred {
|
||||
t.Errorf("Irreducibility test for Polynomial %v failed: got %v, wanted %v",
|
||||
test.f, test.f.Irreducible(), test.irred)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPolIrreducible(b *testing.B) {
|
||||
// find first irreducible polynomial
|
||||
var pol Pol
|
||||
for _, test := range polIrredTests {
|
||||
if test.irred {
|
||||
pol = test.f
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !pol.Irreducible() {
|
||||
b.Errorf("Irreducibility test for Polynomial %v failed", pol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var polGCDTests = []struct {
|
||||
f1 Pol
|
||||
f2 Pol
|
||||
gcd Pol
|
||||
}{
|
||||
{10, 50, 2},
|
||||
{0, 1, 1},
|
||||
{
|
||||
parseBin("101101001"),
|
||||
parseBin("1010"),
|
||||
parseBin("1"),
|
||||
},
|
||||
{2, 2, 2},
|
||||
{
|
||||
parseBin("1010"),
|
||||
parseBin("11"),
|
||||
parseBin("11"),
|
||||
},
|
||||
{
|
||||
0x8000000000000000,
|
||||
0x8000000000000000,
|
||||
0x8000000000000000,
|
||||
},
|
||||
{
|
||||
parseBin("1100"),
|
||||
parseBin("101"),
|
||||
parseBin("11"),
|
||||
},
|
||||
{
|
||||
parseBin("1100001111"),
|
||||
parseBin("10011"),
|
||||
parseBin("10011"),
|
||||
},
|
||||
{
|
||||
0x3DA3358B4DC173,
|
||||
0x3DA3358B4DC173,
|
||||
0x3DA3358B4DC173,
|
||||
},
|
||||
{
|
||||
0x3DA3358B4DC173,
|
||||
0x230d2259defd,
|
||||
1,
|
||||
},
|
||||
{
|
||||
0x230d2259defd,
|
||||
0x51b492b3eff2,
|
||||
parseBin("10011"),
|
||||
},
|
||||
}
|
||||
|
||||
func TestPolGCD(t *testing.T) {
|
||||
for i, test := range polGCDTests {
|
||||
gcd := test.f1.GCD(test.f2)
|
||||
if test.gcd != gcd {
|
||||
t.Errorf("GCD test %d (%+v) failed: got %v, wanted %v",
|
||||
i, test, gcd, test.gcd)
|
||||
}
|
||||
|
||||
gcd = test.f2.GCD(test.f1)
|
||||
if test.gcd != gcd {
|
||||
t.Errorf("GCD test %d (%+v) failed: got %v, wanted %v",
|
||||
i, test, gcd, test.gcd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var polMulModTests = []struct {
|
||||
f1 Pol
|
||||
f2 Pol
|
||||
g Pol
|
||||
mod Pol
|
||||
}{
|
||||
{
|
||||
0x1230,
|
||||
0x230,
|
||||
0x55,
|
||||
0x22,
|
||||
},
|
||||
{
|
||||
0x0eae8c07dbbb3026,
|
||||
0xd5d6db9de04771de,
|
||||
0xdd2bda3b77c9,
|
||||
0x425ae8595b7a,
|
||||
},
|
||||
}
|
||||
|
||||
func TestPolMulMod(t *testing.T) {
|
||||
for i, test := range polMulModTests {
|
||||
mod := test.f1.MulMod(test.f2, test.g)
|
||||
if mod != test.mod {
|
||||
t.Errorf("MulMod test %d (%+v) failed: got %v, wanted %v",
|
||||
i, test, mod, test.mod)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ var cleanupHandlers struct {
|
||||
var stderr = os.Stderr
|
||||
|
||||
func init() {
|
||||
cleanupHandlers.ch = make(chan os.Signal)
|
||||
cleanupHandlers.ch = make(chan os.Signal, 1)
|
||||
go CleanupHandler(cleanupHandlers.ch)
|
||||
signal.Notify(cleanupHandlers.ch, syscall.SIGINT)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/textfile"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/jsonstatus"
|
||||
"github.com/restic/restic/internal/ui/json"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
@@ -35,6 +35,14 @@ var cmdBackup = &cobra.Command{
|
||||
Long: `
|
||||
The "backup" command creates a new snapshot and saves the files and directories
|
||||
given as the arguments.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
|
||||
Note that some issues such as unreadable or deleted files during backup
|
||||
currently doesn't result in a non-zero error exit status.
|
||||
`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
if backupOptions.Host == "" {
|
||||
@@ -95,24 +103,24 @@ func init() {
|
||||
cmdRoot.AddCommand(cmdBackup)
|
||||
|
||||
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.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.StringArrayVar(&backupOptions.InsensitiveExcludes, "iexclude", nil, "same as --exclude `pattern` but ignores the casing of filenames")
|
||||
f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
|
||||
f.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.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", "`filename` to use when reading from stdin")
|
||||
f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
|
||||
|
||||
f.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
|
||||
f.StringVar(&backupOptions.Host, "hostname", "", "set the `hostname` for the snapshot manually")
|
||||
f.MarkDeprecated("hostname", "use --host")
|
||||
|
||||
f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from file (can be combined with file args/can be specified multiple times)")
|
||||
f.StringVar(&backupOptions.TimeStamp, "time", "", "time of the backup (ex. '2012-11-01 22:08:41') (default: now)")
|
||||
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.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")
|
||||
}
|
||||
@@ -374,7 +382,7 @@ func findParentSnapshot(ctx context.Context, repo restic.Repository, opts Backup
|
||||
|
||||
// Find last snapshot to set it as parent, if not already set
|
||||
if !opts.Force && parentID == nil {
|
||||
id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, opts.Host)
|
||||
id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, []string{opts.Host})
|
||||
if err == nil {
|
||||
parentID = &id
|
||||
} else if err != restic.ErrNoSnapshotFound {
|
||||
@@ -439,7 +447,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
|
||||
var p ArchiveProgressReporter
|
||||
if gopts.JSON {
|
||||
p = jsonstatus.NewBackup(term, gopts.verbosity)
|
||||
p = json.NewBackup(term, gopts.verbosity)
|
||||
} else {
|
||||
p = ui.NewBackup(term, gopts.verbosity)
|
||||
}
|
||||
@@ -593,19 +601,18 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||
}
|
||||
|
||||
p.Finish(id)
|
||||
if !gopts.JSON {
|
||||
p.P("snapshot %s saved\n", id.Str())
|
||||
}
|
||||
|
||||
// cleanly shutdown all running goroutines
|
||||
t.Kill(nil)
|
||||
|
||||
// let's see if one returned an error
|
||||
err = t.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
// Report finished execution
|
||||
p.Finish(id)
|
||||
if !gopts.JSON {
|
||||
p.P("snapshot %s saved\n", id.Str())
|
||||
}
|
||||
|
||||
return nil
|
||||
// Return error if any
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ var cmdCache = &cobra.Command{
|
||||
Short: "Operate on local cache directories",
|
||||
Long: `
|
||||
The "cache" command allows listing and cleaning local cache directories.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -18,6 +18,11 @@ var cmdCat = &cobra.Command{
|
||||
Short: "Print internal objects to stdout",
|
||||
Long: `
|
||||
The "cat" command is used to print internal objects to stdout.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -165,18 +170,15 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
|
||||
case "blob":
|
||||
for _, t := range []restic.BlobType{restic.DataBlob, restic.TreeBlob} {
|
||||
list, found := repo.Index().Lookup(id, t)
|
||||
_, found := repo.Index().Lookup(id, t)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
blob := list[0]
|
||||
|
||||
buf := make([]byte, blob.Length)
|
||||
n, err := repo.LoadBlob(gopts.ctx, t, id, buf)
|
||||
buf, err := repo.LoadBlob(gopts.ctx, t, id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
_, err = os.Stdout.Write(buf)
|
||||
return err
|
||||
|
||||
@@ -25,6 +25,11 @@ finds. It can also be used to read all data and therefore simulate a restore.
|
||||
|
||||
By default, the "check" command will always load all data directly from the
|
||||
repository and not use a local cache.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -27,7 +26,13 @@ var cmdDebugDump = &cobra.Command{
|
||||
Short: "Dump data structures",
|
||||
Long: `
|
||||
The "dump" command dumps data structures from the repository as JSON objects. It
|
||||
is used for debugging purposes only.`,
|
||||
is used for debugging purposes only.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDebugDump(globalOptions, args)
|
||||
@@ -84,7 +89,7 @@ func printPacks(repo *repository.Repository, wr io.Writer) error {
|
||||
|
||||
blobs, err := pack.List(repo.Key(), restic.ReaderAt(repo.Backend(), h), size)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error for pack %v: %v\n", id.Str(), err)
|
||||
fmt.Fprintf(globalOptions.stderr, "error for pack %v: %v\n", id.Str(), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,13 +106,11 @@ func printPacks(repo *repository.Repository, wr io.Writer) error {
|
||||
}
|
||||
}
|
||||
|
||||
return prettyPrintJSON(os.Stdout, p)
|
||||
return prettyPrintJSON(wr, p)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dumpIndexes(repo restic.Repository) error {
|
||||
func dumpIndexes(repo restic.Repository, wr io.Writer) error {
|
||||
return repo.List(context.TODO(), restic.IndexFile, func(id restic.ID, size int64) error {
|
||||
fmt.Printf("index_id: %v\n", id)
|
||||
|
||||
@@ -116,7 +119,7 @@ func dumpIndexes(repo restic.Repository) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return idx.Dump(os.Stdout)
|
||||
return idx.Dump(wr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,29 +141,24 @@ func runDebugDump(gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(gopts.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpe := args[0]
|
||||
|
||||
switch tpe {
|
||||
case "indexes":
|
||||
return dumpIndexes(repo)
|
||||
return dumpIndexes(repo, gopts.stdout)
|
||||
case "snapshots":
|
||||
return debugPrintSnapshots(repo, os.Stdout)
|
||||
return debugPrintSnapshots(repo, gopts.stdout)
|
||||
case "packs":
|
||||
return printPacks(repo, os.Stdout)
|
||||
return printPacks(repo, gopts.stdout)
|
||||
case "all":
|
||||
fmt.Printf("snapshots:\n")
|
||||
err := debugPrintSnapshots(repo, os.Stdout)
|
||||
err := debugPrintSnapshots(repo, gopts.stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("\nindexes:\n")
|
||||
err = dumpIndexes(repo)
|
||||
err = dumpIndexes(repo, gopts.stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ directory:
|
||||
* U The metadata (access mode, timestamps, ...) for the item was updated
|
||||
* M The file's content was modified
|
||||
* T The type was changed, e.g. a file was made a symlink
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -66,7 +71,7 @@ type Comparer struct {
|
||||
type DiffStat struct {
|
||||
Files, Dirs, Others int
|
||||
DataBlobs, TreeBlobs int
|
||||
Bytes int
|
||||
Bytes uint64
|
||||
}
|
||||
|
||||
// Add adds stats information for node to s.
|
||||
@@ -141,7 +146,7 @@ func updateBlobs(repo restic.Repository, blobs restic.BlobSet, stats *DiffStat)
|
||||
continue
|
||||
}
|
||||
|
||||
stats.Bytes += int(size)
|
||||
stats.Bytes += uint64(size)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ prints its contents to stdout.
|
||||
|
||||
The special snapshot "latest" can be used to use the latest snapshot in the
|
||||
repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -36,7 +41,7 @@ repository.
|
||||
|
||||
// DumpOptions collects all options for the dump command.
|
||||
type DumpOptions struct {
|
||||
Host string
|
||||
Hosts []string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
}
|
||||
@@ -47,7 +52,7 @@ func init() {
|
||||
cmdRoot.AddCommand(cmdDump)
|
||||
|
||||
flags := cmdDump.Flags()
|
||||
flags.StringVarP(&dumpOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`)
|
||||
flags.StringArrayVarP(&dumpOptions.Hosts, "host", "H", nil, `only consider snapshots for this host when the snapshot ID is "latest" (can be specified multiple times)`)
|
||||
flags.Var(&dumpOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"")
|
||||
flags.StringArrayVar(&dumpOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
|
||||
}
|
||||
@@ -136,9 +141,9 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
||||
var id restic.ID
|
||||
|
||||
if snapshotIDString == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host)
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Hosts)
|
||||
if err != nil {
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Hosts:%v", err, opts.Paths, opts.Hosts)
|
||||
}
|
||||
} else {
|
||||
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||
@@ -166,24 +171,15 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
func getNodeData(ctx context.Context, output io.Writer, repo restic.Repository, node *restic.Node) error {
|
||||
var buf []byte
|
||||
var (
|
||||
buf []byte
|
||||
err error
|
||||
)
|
||||
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)
|
||||
buf, err = repo.LoadBlob(ctx, restic.DataBlob, id, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
_, err = output.Write(buf)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,7 +27,13 @@ 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`,
|
||||
restic find --pack 025c1d06
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runFind(findOptions, globalOptions, args)
|
||||
@@ -45,7 +51,7 @@ type FindOptions struct {
|
||||
PackID, ShowPackID bool
|
||||
CaseInsensitive bool
|
||||
ListLong bool
|
||||
Host string
|
||||
Hosts []string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
}
|
||||
@@ -66,7 +72,7 @@ func init() {
|
||||
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.StringVarP(&findOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
|
||||
f.StringArrayVarP(&findOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
||||
f.Var(&findOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given")
|
||||
f.StringArrayVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
|
||||
}
|
||||
@@ -150,7 +156,7 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
||||
if s.hits > 0 {
|
||||
Printf(",")
|
||||
}
|
||||
Printf(string(b))
|
||||
Print(string(b))
|
||||
s.hits++
|
||||
}
|
||||
|
||||
@@ -160,9 +166,9 @@ func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) {
|
||||
Verbosef("\n")
|
||||
}
|
||||
s.oldsn = s.newsn
|
||||
Verbosef("Found matching entries in snapshot %s\n", s.oldsn.ID().Str())
|
||||
Verbosef("Found matching entries in snapshot %s from %s\n", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(TimeFormat))
|
||||
}
|
||||
Printf(formatNode(path, node, s.ListLong) + "\n")
|
||||
Println(formatNode(path, node, s.ListLong))
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintPattern(path string, node *restic.Node) {
|
||||
@@ -201,7 +207,7 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *
|
||||
if s.hits > 0 {
|
||||
Printf(",")
|
||||
}
|
||||
Printf(string(b))
|
||||
Print(string(b))
|
||||
s.hits++
|
||||
}
|
||||
|
||||
@@ -561,7 +567,7 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
||||
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.Hosts, 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
|
||||
|
||||
@@ -16,7 +16,13 @@ var cmdForget = &cobra.Command{
|
||||
The "forget" command removes snapshots according to a policy. Please note that
|
||||
this command really only deletes the snapshot object in the repository, which
|
||||
is a reference to data stored there. In order to remove this (now unreferenced)
|
||||
data after 'forget' was run successfully, see the 'prune' command. `,
|
||||
data after 'forget' was run successfully, see the 'prune' command.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runForget(forgetOptions, globalOptions, args)
|
||||
@@ -34,7 +40,7 @@ type ForgetOptions struct {
|
||||
Within restic.Duration
|
||||
KeepTags restic.TagLists
|
||||
|
||||
Host string
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Compact bool
|
||||
@@ -60,8 +66,8 @@ func init() {
|
||||
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||
|
||||
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
|
||||
f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
|
||||
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname`")
|
||||
f.StringArrayVar(&forgetOptions.Hosts, "host", nil, "only consider snapshots with the given `host` (can be specified multiple times)")
|
||||
f.StringArrayVar(&forgetOptions.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)")
|
||||
f.MarkDeprecated("hostname", "use --host")
|
||||
|
||||
f.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")
|
||||
@@ -95,7 +101,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||
|
||||
var snapshots restic.Snapshots
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ var cmdGenerate = &cobra.Command{
|
||||
Long: `
|
||||
The "generate" command writes automatically generated files (like the man pages
|
||||
and the auto-completion files for bash and zsh).
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: runGenerate,
|
||||
|
||||
@@ -12,6 +12,11 @@ var cmdInit = &cobra.Command{
|
||||
Short: "Initialize a new repository",
|
||||
Long: `
|
||||
The "init" command initializes a new repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -20,6 +20,11 @@ var cmdKey = &cobra.Command{
|
||||
Short: "Manage keys (passwords)",
|
||||
Long: `
|
||||
The "key" command manages keys (passwords) for accessing the repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -15,6 +15,11 @@ var cmdList = &cobra.Command{
|
||||
Short: "List objects in the repository",
|
||||
Long: `
|
||||
The "list" command allows listing objects in the repository based on type.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -33,6 +33,11 @@ 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.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -43,7 +48,7 @@ a path separator); paths use the forward slash '/' as separator.
|
||||
// LsOptions collects all options for the ls command.
|
||||
type LsOptions struct {
|
||||
ListLong bool
|
||||
Host string
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Recursive bool
|
||||
@@ -56,7 +61,7 @@ func init() {
|
||||
|
||||
flags := cmdLs.Flags()
|
||||
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
|
||||
flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
|
||||
flags.StringArrayVarP(&lsOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
||||
flags.Var(&lsOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given")
|
||||
flags.StringArrayVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
|
||||
flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories")
|
||||
@@ -84,7 +89,7 @@ type lsNode struct {
|
||||
}
|
||||
|
||||
func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
if len(args) == 0 && opts.Host == "" && len(opts.Tags) == 0 && len(opts.Paths) == 0 {
|
||||
if len(args) == 0 && len(opts.Hosts) == 0 && len(opts.Tags) == 0 && len(opts.Paths) == 0 {
|
||||
return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
|
||||
}
|
||||
|
||||
@@ -186,7 +191,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, 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) {
|
||||
|
||||
@@ -13,6 +13,11 @@ var cmdMigrate = &cobra.Command{
|
||||
Long: `
|
||||
The "migrate" command applies migrations to a repository. When no migration
|
||||
name is explicitly given, a list of migrations that can be applied is printed.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -44,6 +44,11 @@ You need to specify a sample format for exactly the following timestamp:
|
||||
|
||||
For details please see the documentation for time.Format() at:
|
||||
https://godoc.org/time#Time.Format
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -54,10 +59,9 @@ For details please see the documentation for time.Format() at:
|
||||
// MountOptions collects all options for the mount command.
|
||||
type MountOptions struct {
|
||||
OwnerRoot bool
|
||||
AllowRoot bool
|
||||
AllowOther bool
|
||||
NoDefaultPermissions bool
|
||||
Host string
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
SnapshotTemplate string
|
||||
@@ -70,11 +74,10 @@ func init() {
|
||||
|
||||
mountFlags := cmdMount.Flags()
|
||||
mountFlags.BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
|
||||
mountFlags.BoolVar(&mountOptions.AllowRoot, "allow-root", false, "allow root user to access the data in the mounted directory")
|
||||
mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory")
|
||||
mountFlags.BoolVar(&mountOptions.NoDefaultPermissions, "no-default-permissions", false, "for 'allow-other', ignore Unix permissions and allow users to read all snapshot files")
|
||||
|
||||
mountFlags.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`)
|
||||
mountFlags.StringArrayVarP(&mountOptions.Hosts, "host", "H", nil, `only consider snapshots for this host (can be specified multiple times)`)
|
||||
mountFlags.Var(&mountOptions.Tags, "tag", "only consider snapshots which include this `taglist`")
|
||||
mountFlags.StringArrayVar(&mountOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`")
|
||||
|
||||
@@ -114,10 +117,6 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
systemFuse.FSName("restic"),
|
||||
}
|
||||
|
||||
if opts.AllowRoot {
|
||||
mountOptions = append(mountOptions, systemFuse.AllowRoot())
|
||||
}
|
||||
|
||||
if opts.AllowOther {
|
||||
mountOptions = append(mountOptions, systemFuse.AllowOther())
|
||||
|
||||
@@ -138,7 +137,7 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
|
||||
cfg := fuse.Config{
|
||||
OwnerIsRoot: opts.OwnerRoot,
|
||||
Host: opts.Host,
|
||||
Hosts: opts.Hosts,
|
||||
Tags: opts.Tags,
|
||||
Paths: opts.Paths,
|
||||
SnapshotTemplate: opts.SnapshotTemplate,
|
||||
|
||||
@@ -13,6 +13,11 @@ var optionsCmd = &cobra.Command{
|
||||
Short: "Print list of extended options",
|
||||
Long: `
|
||||
The "options" command prints a list of extended options.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
Hidden: true,
|
||||
DisableAutoGenTag: true,
|
||||
|
||||
@@ -19,6 +19,11 @@ var cmdPrune = &cobra.Command{
|
||||
Long: `
|
||||
The "prune" command checks the repository and removes data that is not
|
||||
referenced and therefore not needed any more.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -16,6 +16,11 @@ var cmdRebuildIndex = &cobra.Command{
|
||||
Long: `
|
||||
The "rebuild-index" command creates a new index based on the pack files in the
|
||||
repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -57,11 +62,17 @@ func rebuildIndex(ctx context.Context, repo restic.Repository, ignorePacks resti
|
||||
}
|
||||
|
||||
bar := newProgressMax(!globalOptions.Quiet, packs-uint64(len(ignorePacks)), "packs")
|
||||
idx, _, err := index.New(ctx, repo, ignorePacks, bar)
|
||||
idx, invalidFiles, err := index.New(ctx, repo, ignorePacks, bar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if globalOptions.verbosity >= 2 {
|
||||
for _, id := range invalidFiles {
|
||||
Printf("skipped incomplete pack file: %v\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
Verbosef("finding old index files\n")
|
||||
|
||||
var supersedes restic.IDs
|
||||
|
||||
@@ -16,6 +16,11 @@ var cmdRecover = &cobra.Command{
|
||||
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".
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/filter"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/restorer"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -20,6 +21,11 @@ a directory.
|
||||
|
||||
The special snapshot "latest" can be used to restore the latest snapshot in the
|
||||
repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -34,7 +40,7 @@ type RestoreOptions struct {
|
||||
Include []string
|
||||
InsensitiveInclude []string
|
||||
Target string
|
||||
Host string
|
||||
Hosts []string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
Verify bool
|
||||
@@ -52,7 +58,7 @@ func init() {
|
||||
flags.StringArrayVar(&restoreOptions.InsensitiveInclude, "iinclude", nil, "same as `--include` but ignores the casing of filenames")
|
||||
flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to")
|
||||
|
||||
flags.StringVarP(&restoreOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`)
|
||||
flags.StringArrayVarP(&restoreOptions.Hosts, "host", "H", nil, `only consider snapshots for this host when the snapshot ID is "latest" (can be specified multiple times)`)
|
||||
flags.Var(&restoreOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"")
|
||||
flags.StringArrayVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
|
||||
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
|
||||
@@ -111,9 +117,9 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||
var id restic.ID
|
||||
|
||||
if snapshotIDString == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host)
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Hosts)
|
||||
if err != nil {
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Hosts:%v", err, opts.Paths, opts.Hosts)
|
||||
}
|
||||
} else {
|
||||
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||
|
||||
@@ -18,6 +18,11 @@ 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.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -18,6 +18,11 @@ var cmdSnapshots = &cobra.Command{
|
||||
Short: "List all snapshots",
|
||||
Long: `
|
||||
The "snapshots" command lists all snapshots stored in the repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -27,7 +32,7 @@ The "snapshots" command lists all snapshots stored in the repository.
|
||||
|
||||
// SnapshotOptions bundles all options for the snapshots command.
|
||||
type SnapshotOptions struct {
|
||||
Host string
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Compact bool
|
||||
@@ -41,7 +46,7 @@ func init() {
|
||||
cmdRoot.AddCommand(cmdSnapshots)
|
||||
|
||||
f := cmdSnapshots.Flags()
|
||||
f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`")
|
||||
f.StringArrayVarP(&snapshotOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host` (can be specified multiple times)")
|
||||
f.Var(&snapshotOptions.Tags, "tag", "only consider snapshots which include this `taglist` (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")
|
||||
@@ -67,7 +72,7 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
|
||||
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.Hosts, opts.Tags, opts.Paths, args) {
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
snapshotGroups, grouped, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
||||
|
||||
@@ -38,6 +38,11 @@ The modes are:
|
||||
* blobs-per-file: A combination of files-by-contents and raw-data.
|
||||
|
||||
Refer to the online manual for more details about each mode.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -49,7 +54,7 @@ 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")
|
||||
f.StringArrayVarP(&snapshotByHosts, "host", "H", nil, "filter latest snapshot by this hostname (can be specified multiple times)")
|
||||
}
|
||||
|
||||
func runStats(gopts GlobalOptions, args []string) error {
|
||||
@@ -84,10 +89,11 @@ func runStats(gopts GlobalOptions, args []string) error {
|
||||
|
||||
// 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(),
|
||||
uniqueFiles: make(map[fileID]struct{}),
|
||||
uniqueInodes: make(map[uint64]struct{}),
|
||||
fileBlobs: make(map[string]restic.IDSet),
|
||||
blobs: restic.NewBlobSet(),
|
||||
blobsSeen: restic.NewBlobSet(),
|
||||
}
|
||||
|
||||
if snapshotIDString != "" {
|
||||
@@ -95,7 +101,7 @@ func runStats(gopts GlobalOptions, args []string) error {
|
||||
|
||||
var sID restic.ID
|
||||
if snapshotIDString == "latest" {
|
||||
sID, err = restic.FindLatestSnapshot(ctx, repo, []string{}, []restic.TagList{}, snapshotByHost)
|
||||
sID, err = restic.FindLatestSnapshot(ctx, repo, []string{}, []restic.TagList{}, snapshotByHosts)
|
||||
if err != nil {
|
||||
return errors.Fatalf("latest snapshot for criteria not found: %v", err)
|
||||
}
|
||||
@@ -112,6 +118,9 @@ func runStats(gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
err = statsWalkSnapshot(ctx, snapshot, repo, stats)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error walking snapshot: %v", err)
|
||||
}
|
||||
} else {
|
||||
// iterate every snapshot in the repo
|
||||
err = repo.List(ctx, restic.SnapshotFile, func(snapshotID restic.ID, size int64) error {
|
||||
@@ -185,7 +194,7 @@ func statsWalkSnapshot(ctx context.Context, snapshot *restic.Snapshot, repo rest
|
||||
}
|
||||
|
||||
func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFunc {
|
||||
return func(_ restic.ID, npath string, node *restic.Node, nodeErr error) (bool, error) {
|
||||
return func(parentTreeID restic.ID, npath string, node *restic.Node, nodeErr error) (bool, error) {
|
||||
if nodeErr != nil {
|
||||
return true, nodeErr
|
||||
}
|
||||
@@ -220,7 +229,7 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun
|
||||
// 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)
|
||||
return true, fmt.Errorf("blob %s not found for tree %s", blobID, parentTreeID)
|
||||
}
|
||||
|
||||
// count the blob's size, then add this blob by this
|
||||
@@ -239,8 +248,16 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun
|
||||
// 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++
|
||||
|
||||
// if inodes are present, only count each inode once
|
||||
// (hard links do not increase restore size)
|
||||
if _, ok := stats.uniqueInodes[node.Inode]; !ok || node.Inode == 0 {
|
||||
stats.uniqueInodes[node.Inode] = struct{}{}
|
||||
stats.TotalSize += node.Size
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -293,6 +310,10 @@ type statsContainer struct {
|
||||
// contents (hashed sequence of content blob IDs)
|
||||
uniqueFiles map[fileID]struct{}
|
||||
|
||||
// uniqueInodes marks visited files according to their
|
||||
// inode # (hashed sequence of inode numbers)
|
||||
uniqueInodes map[uint64]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
|
||||
@@ -314,7 +335,7 @@ var (
|
||||
|
||||
// snapshotByHost is the host to filter latest
|
||||
// snapshot by, if given by user
|
||||
snapshotByHost string
|
||||
snapshotByHosts []string
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -21,6 +21,11 @@ You can either set/replace the entire set of tags on a snapshot, or
|
||||
add tags to/remove tags from the existing set.
|
||||
|
||||
When no snapshot-ID is given, all snapshots matching the host, tag and path filter criteria are modified.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -30,7 +35,7 @@ When no snapshot-ID is given, all snapshots matching the host, tag and path filt
|
||||
|
||||
// TagOptions bundles all options for the 'tag' command.
|
||||
type TagOptions struct {
|
||||
Host string
|
||||
Hosts []string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
SetTags []string
|
||||
@@ -48,7 +53,7 @@ func init() {
|
||||
tagFlags.StringSliceVar(&tagOptions.AddTags, "add", nil, "`tag` which will be added to the existing tags (can be given multiple times)")
|
||||
tagFlags.StringSliceVar(&tagOptions.RemoveTags, "remove", nil, "`tag` which will be removed from the existing tags (can be given multiple times)")
|
||||
|
||||
tagFlags.StringVarP(&tagOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
|
||||
tagFlags.StringArrayVarP(&tagOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
||||
tagFlags.Var(&tagOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given")
|
||||
tagFlags.StringArrayVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
|
||||
}
|
||||
@@ -124,7 +129,7 @@ func runTag(opts TagOptions, gopts GlobalOptions, args []string) error {
|
||||
changeCnt := 0
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||
changed, err := changeTags(ctx, repo, sn, opts.SetTags, opts.AddTags, opts.RemoveTags)
|
||||
if err != nil {
|
||||
Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err)
|
||||
|
||||
@@ -10,6 +10,11 @@ var unlockCmd = &cobra.Command{
|
||||
Short: "Remove locks other processes created",
|
||||
Long: `
|
||||
The "unlock" command removes stale locks that have been created by other restic processes.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -13,6 +13,11 @@ var versionCmd = &cobra.Command{
|
||||
Long: `
|
||||
The "version" command prints detailed information about the build environment
|
||||
and the version of this software.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots.
|
||||
func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, host string, tags []restic.TagList, paths []string, snapshotIDs []string) <-chan *restic.Snapshot {
|
||||
func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hosts []string, tags []restic.TagList, paths []string, snapshotIDs []string) <-chan *restic.Snapshot {
|
||||
out := make(chan *restic.Snapshot)
|
||||
go func() {
|
||||
defer close(out)
|
||||
@@ -22,9 +22,9 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
|
||||
// Process all snapshot IDs given as arguments.
|
||||
for _, s := range snapshotIDs {
|
||||
if s == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, paths, tags, host)
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, paths, tags, hosts)
|
||||
if err != nil {
|
||||
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Host:%v)\n", s, paths, tags, host)
|
||||
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Hosts:%v)\n", s, paths, tags, hosts)
|
||||
usedFilter = true
|
||||
continue
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
|
||||
}
|
||||
|
||||
// Give the user some indication their filters are not used.
|
||||
if !usedFilter && (host != "" || len(tags) != 0 || len(paths) != 0) {
|
||||
if !usedFilter && (len(hosts) != 0 || len(tags) != 0 || len(paths) != 0) {
|
||||
Warnf("Ignoring filters as there are explicit snapshot ids given\n")
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
|
||||
return
|
||||
}
|
||||
|
||||
snapshots, err := restic.FindFilteredSnapshots(ctx, repo, host, tags, paths)
|
||||
snapshots, err := restic.FindFilteredSnapshots(ctx, repo, hosts, tags, paths)
|
||||
if err != nil {
|
||||
Warnf("could not load snapshots: %v\n", err)
|
||||
return
|
||||
|
||||
@@ -39,7 +39,7 @@ import (
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
var version = "0.9.6"
|
||||
var version = "0.9.6-dev (compiled manually)"
|
||||
|
||||
// TimeFormat is the format used for all timestamps printed by restic.
|
||||
const TimeFormat = "2006-01-02 15:04:05"
|
||||
@@ -85,6 +85,8 @@ var globalOptions = GlobalOptions{
|
||||
stderr: os.Stderr,
|
||||
}
|
||||
|
||||
var isReadingPassword bool
|
||||
|
||||
func init() {
|
||||
var cancel context.CancelFunc
|
||||
globalOptions.ctx, cancel = context.WithCancel(context.Background())
|
||||
@@ -94,18 +96,18 @@ func init() {
|
||||
})
|
||||
|
||||
f := cmdRoot.PersistentFlags()
|
||||
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
||||
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
|
||||
f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
|
||||
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "specify a shell command to obtain a password (default: $RESTIC_PASSWORD_COMMAND)")
|
||||
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "`repository` to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
||||
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a `file` (default: $RESTIC_PASSWORD_FILE)")
|
||||
f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "`key` ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
|
||||
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "specify a shell `command` to obtain a password (default: $RESTIC_PASSWORD_COMMAND)")
|
||||
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
|
||||
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
|
||||
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
|
||||
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
|
||||
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory. (default: use system default cache directory)")
|
||||
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache `directory`. (default: use system default cache directory)")
|
||||
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
|
||||
f.StringSliceVar(&globalOptions.CACerts, "cacert", nil, "`file` to load root certificates from (default: use system certificates)")
|
||||
f.StringVar(&globalOptions.TLSClientCert, "tls-client-cert", "", "path to a file containing PEM encoded TLS client certificate and private key")
|
||||
f.StringVar(&globalOptions.TLSClientCert, "tls-client-cert", "", "path to a `file` containing PEM encoded TLS client certificate and private key")
|
||||
f.BoolVar(&globalOptions.CleanupCache, "cleanup-cache", false, "auto remove old cache directories")
|
||||
f.IntVar(&globalOptions.LimitUploadKb, "limit-upload", 0, "limits uploads to a maximum rate in KiB/s. (default: unlimited)")
|
||||
f.IntVar(&globalOptions.LimitDownloadKb, "limit-download", 0, "limits downloads to a maximum rate in KiB/s. (default: unlimited)")
|
||||
@@ -146,7 +148,10 @@ func stdoutTerminalWidth() int {
|
||||
}
|
||||
|
||||
// restoreTerminal installs a cleanup handler that restores the previous
|
||||
// terminal state on exit.
|
||||
// terminal state on exit. This handler is only intended to restore the
|
||||
// terminal configuration if restic exits after receiving a signal. A regular
|
||||
// program execution must revert changes to the terminal configuration itself.
|
||||
// The terminal configuration is only restored while reading a password.
|
||||
func restoreTerminal() {
|
||||
if !stdoutIsTerminal() {
|
||||
return
|
||||
@@ -160,9 +165,17 @@ func restoreTerminal() {
|
||||
}
|
||||
|
||||
AddCleanupHandler(func() error {
|
||||
// Restoring the terminal configuration while restic runs in the
|
||||
// background, causes restic to get stopped on unix systems with
|
||||
// a SIGTTOU signal. Thus only restore the terminal settings if
|
||||
// they might have been modified, which is the case while reading
|
||||
// a password.
|
||||
if !isReadingPassword {
|
||||
return nil
|
||||
}
|
||||
err := checkErrno(terminal.Restore(fd, state))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to get restore terminal state: %#+v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
@@ -189,6 +202,22 @@ func Printf(format string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Print writes the message to the configured stdout stream.
|
||||
func Print(args ...interface{}) {
|
||||
_, err := fmt.Fprint(globalOptions.stdout, args...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Println writes the message to the configured stdout stream.
|
||||
func Println(args ...interface{}) {
|
||||
_, err := fmt.Fprintln(globalOptions.stdout, args...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verbosef calls Printf to write the message when the verbose flag is set.
|
||||
func Verbosef(format string, args ...interface{}) {
|
||||
if globalOptions.verbosity >= 1 {
|
||||
@@ -232,7 +261,7 @@ func Warnf(format string, args ...interface{}) {
|
||||
// Exitf uses Warnf to write the message and then terminates the process with
|
||||
// the given exit code.
|
||||
func Exitf(exitcode int, format string, args ...interface{}) {
|
||||
if format[len(format)-1] != '\n' {
|
||||
if !(strings.HasSuffix(format, "\n")) {
|
||||
format += "\n"
|
||||
}
|
||||
|
||||
@@ -286,7 +315,9 @@ func readPassword(in io.Reader) (password string, err error) {
|
||||
// password.
|
||||
func readPasswordTerminal(in *os.File, out io.Writer, prompt string) (password string, err error) {
|
||||
fmt.Fprint(out, prompt)
|
||||
isReadingPassword = true
|
||||
buf, err := terminal.ReadPassword(int(in.Fd()))
|
||||
isReadingPassword = false
|
||||
fmt.Fprintln(out)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "ReadPassword")
|
||||
|
||||
28
cmd/restic/global_test.go
Normal file
28
cmd/restic/global_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func Test_PrintFunctionsRespectsGlobalStdout(t *testing.T) {
|
||||
gopts := globalOptions
|
||||
defer func() {
|
||||
globalOptions = gopts
|
||||
}()
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
globalOptions.stdout = buf
|
||||
|
||||
for _, p := range []func(){
|
||||
func() { Println("message") },
|
||||
func() { Print("message\n") },
|
||||
func() { Printf("mes%s\n", "sage") },
|
||||
} {
|
||||
p()
|
||||
rtest.Equals(t, "message\n", buf.String())
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
@@ -94,10 +94,10 @@ func testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID res
|
||||
testRunRestoreExcludes(t, opts, dir, snapshotID, nil)
|
||||
}
|
||||
|
||||
func testRunRestoreLatest(t testing.TB, gopts GlobalOptions, dir string, paths []string, host string) {
|
||||
func testRunRestoreLatest(t testing.TB, gopts GlobalOptions, dir string, paths []string, hosts []string) {
|
||||
opts := RestoreOptions{
|
||||
Target: dir,
|
||||
Host: host,
|
||||
Hosts: hosts,
|
||||
Paths: paths,
|
||||
}
|
||||
|
||||
@@ -765,7 +765,7 @@ func TestRestore(t *testing.T) {
|
||||
|
||||
// Restore latest without any filters
|
||||
restoredir := filepath.Join(env.base, "restore")
|
||||
testRunRestoreLatest(t, env.gopts, restoredir, nil, "")
|
||||
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
|
||||
|
||||
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata))),
|
||||
"directories are not equal")
|
||||
@@ -802,7 +802,7 @@ func TestRestoreLatest(t *testing.T) {
|
||||
testRunCheck(t, env.gopts)
|
||||
|
||||
// Restore latest without any filters
|
||||
testRunRestoreLatest(t, env.gopts, filepath.Join(env.base, "restore0"), nil, "")
|
||||
testRunRestoreLatest(t, env.gopts, filepath.Join(env.base, "restore0"), nil, nil)
|
||||
rtest.OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", "testfile.c"), int64(101)))
|
||||
|
||||
// Setup test files in different directories backed up in different snapshots
|
||||
@@ -823,14 +823,14 @@ func TestRestoreLatest(t *testing.T) {
|
||||
p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c")
|
||||
p2rAbs := filepath.Join(env.base, "restore2", "p2/testfile.c")
|
||||
|
||||
testRunRestoreLatest(t, env.gopts, filepath.Join(env.base, "restore1"), []string{filepath.Dir(p1)}, "")
|
||||
testRunRestoreLatest(t, env.gopts, filepath.Join(env.base, "restore1"), []string{filepath.Dir(p1)}, nil)
|
||||
rtest.OK(t, testFileSize(p1rAbs, int64(102)))
|
||||
if _, err := os.Stat(p2rAbs); os.IsNotExist(errors.Cause(err)) {
|
||||
rtest.Assert(t, os.IsNotExist(errors.Cause(err)),
|
||||
"expected %v to not exist in restore, but it exists, err %v", p2rAbs, err)
|
||||
}
|
||||
|
||||
testRunRestoreLatest(t, env.gopts, filepath.Join(env.base, "restore2"), []string{filepath.Dir(p2)}, "")
|
||||
testRunRestoreLatest(t, env.gopts, filepath.Join(env.base, "restore2"), []string{filepath.Dir(p2)}, nil)
|
||||
rtest.OK(t, testFileSize(p2rAbs, int64(103)))
|
||||
if _, err := os.Stat(p1rAbs); os.IsNotExist(errors.Cause(err)) {
|
||||
rtest.Assert(t, os.IsNotExist(errors.Cause(err)),
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestRestoreLocalLayout(t *testing.T) {
|
||||
|
||||
// restore latest snapshot
|
||||
target := filepath.Join(env.base, "restore")
|
||||
testRunRestoreLatest(t, env.gopts, target, nil, "")
|
||||
testRunRestoreLatest(t, env.gopts, target, nil, nil)
|
||||
|
||||
rtest.RemoveAll(t, filepath.Join(env.base, "repo"))
|
||||
rtest.RemoveAll(t, target)
|
||||
|
||||
@@ -245,7 +245,7 @@ From Source
|
||||
***********
|
||||
|
||||
restic is written in the Go programming language and you need at least
|
||||
Go version 1.9. Building restic may also work with older versions of Go,
|
||||
Go version 1.11. Building restic may also work with older versions of Go,
|
||||
but that's not supported. See the `Getting
|
||||
started <https://golang.org/doc/install>`__ guide of the Go project for
|
||||
instructions how to install Go.
|
||||
@@ -259,13 +259,6 @@ In order to build restic from source, execute the following steps:
|
||||
|
||||
$ cd restic
|
||||
|
||||
$ go run -mod=vendor build.go
|
||||
|
||||
For Go versions < 1.11, the option ``-mod=vendor`` needs to be removed, like
|
||||
this:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ go run build.go
|
||||
|
||||
You can easily cross-compile restic for all supported platforms, just
|
||||
@@ -274,13 +267,11 @@ supply the target OS and platform via the command-line options like this
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ go run -mod=vendor build.go --goos windows --goarch amd64
|
||||
$ go run build.go --goos windows --goarch amd64
|
||||
|
||||
$ go run -mod=vendor build.go --goos freebsd --goarch 386
|
||||
$ go run build.go --goos freebsd --goarch 386
|
||||
|
||||
$ go run -mod=vendor build.go --goos linux --goarch arm --goarm 6
|
||||
|
||||
Again, for Go < 1.11 ``-mod=vendor`` needs to be removed.
|
||||
$ go run build.go --goos linux --goarch arm --goarm 6
|
||||
|
||||
The resulting binary is statically linked and does not require any
|
||||
libraries.
|
||||
@@ -297,7 +288,7 @@ Restic can write out man pages and bash/zsh compatible autocompletion scripts:
|
||||
|
||||
$ ./restic generate --help
|
||||
|
||||
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).
|
||||
|
||||
Usage:
|
||||
|
||||
@@ -54,6 +54,13 @@ command and enter the same password twice:
|
||||
Remembering your password is important! If you lose it, you won't be
|
||||
able to access data stored in the repository.
|
||||
|
||||
.. warning::
|
||||
|
||||
On Linux, storing the backup repository on a CIFS (SMB) share is not
|
||||
recommended due to compatibility issues. Either use another backend
|
||||
or set the environment variable `GODEBUG` to `asyncpreemptoff=1`.
|
||||
Refer to GitHub issue #2659 for further explanations.
|
||||
|
||||
SFTP
|
||||
****
|
||||
|
||||
@@ -78,15 +85,29 @@ You can also specify a relative (read: no slash (``/``) character at the
|
||||
beginning) directory, in this case the dir is relative to the remote
|
||||
user's home directory.
|
||||
|
||||
Also, if the SFTP server is enforcing domain-confined users, you can
|
||||
specify the user this way: ``user@domain@host``.
|
||||
|
||||
.. note:: Please be aware that sftp servers do not expand the tilde character
|
||||
(``~``) normally used as an alias for a user's home directory. If you
|
||||
want to specify a path relative to the user's home directory, pass a
|
||||
relative path to the sftp backend.
|
||||
|
||||
The backend config string does not allow specifying a port. If you need
|
||||
to contact an sftp server on a different port, you can create an entry
|
||||
in the ``ssh`` file, usually located in your user's home directory at
|
||||
``~/.ssh/config`` or in ``/etc/ssh/ssh_config``:
|
||||
If you need to specify a port number or IPv6 address, you'll need to use
|
||||
URL syntax. E.g., the repository ``/srv/restic-repo`` on ``[::1]`` (localhost)
|
||||
at port 2222 with username ``user`` can be specified as
|
||||
|
||||
::
|
||||
|
||||
sftp://user@[::1]:2222//srv/restic-repo
|
||||
|
||||
Note the double slash: the first slash separates the connection settings from
|
||||
the path, while the second is the start of the path. To specify a relative
|
||||
path, use one slash.
|
||||
|
||||
Alternatively, you can create an entry in the ``ssh`` configuration file,
|
||||
usually located in your home directory at ``~/.ssh/config`` or in
|
||||
``/etc/ssh/ssh_config``:
|
||||
|
||||
::
|
||||
|
||||
@@ -235,7 +256,7 @@ credentials of your Minio Server.
|
||||
$ export AWS_ACCESS_KEY_ID=<YOUR-MINIO-ACCESS-KEY-ID>
|
||||
$ export AWS_SECRET_ACCESS_KEY= <YOUR-MINIO-SECRET-ACCESS-KEY>
|
||||
|
||||
Now you can easily initialize restic to use Minio server as backend with
|
||||
Now you can easily initialize restic to use Minio server as a backend with
|
||||
this command.
|
||||
|
||||
.. code-block:: console
|
||||
@@ -247,6 +268,35 @@ this command.
|
||||
Please note that knowledge of your password is required to access
|
||||
the repository. Losing your password means that your data is irrecoverably lost.
|
||||
|
||||
Wasabi
|
||||
************
|
||||
|
||||
`Wasabi <https://wasabi.com>`__ is a low cost AWS S3 conformant object storage provider.
|
||||
Due to it's S3 conformance, Wasabi can be used as a storage provider for a restic repository.
|
||||
|
||||
- Create a Wasabi bucket using the `Wasabi Console <https://console.wasabisys.com>`__.
|
||||
- Determine the correct Wasabi service URL for your bucket `here <https://wasabi-support.zendesk.com/hc/en-us/articles/360015106031-What-are-the-service-URLs-for-Wasabi-s-different-regions->`__.
|
||||
|
||||
You must first setup the following environment variables with the
|
||||
credentials of your Wasabi account.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export AWS_ACCESS_KEY_ID=<YOUR-WASABI-ACCESS-KEY-ID>
|
||||
$ export AWS_SECRET_ACCESS_KEY=<YOUR-WASABI-SECRET-ACCESS-KEY>
|
||||
|
||||
Now you can easily initialize restic to use Wasabi as a backend with
|
||||
this command.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./restic -r s3:https://<WASABI-SERVICE-URL>/<WASABI-BUCKET-NAME> init
|
||||
enter password for new backend:
|
||||
enter password again:
|
||||
created restic backend xxxxxxxxxx at s3:https://<WASABI-SERVICE-URL>/<WASABI-BUCKET-NAME>
|
||||
Please note that knowledge of your password is required to access
|
||||
the repository. Losing your password means that your data is irrecoverably lost.
|
||||
|
||||
OpenStack Swift
|
||||
***************
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ even if restic is passed a relative path to save.
|
||||
|
||||
Environment-variables in exclude files are expanded with `os.ExpandEnv <https://golang.org/pkg/os/#ExpandEnv>`__,
|
||||
so ``/home/$USER/foo`` will be expanded to ``/home/bob/foo`` for the user ``bob``.
|
||||
To get a literal dollar sign, write ``$$`` to the file.
|
||||
To get a literal dollar sign, write ``$$`` to the file. Note that tilde (``~``) expansion does not work, please use the ``$HOME`` environment variable instead.
|
||||
|
||||
Patterns need to match on complete path components. For example, the pattern ``foo``:
|
||||
|
||||
|
||||
@@ -82,54 +82,85 @@ Furthermore you can group the output by the same filters (host, paths, tags):
|
||||
1 snapshots
|
||||
|
||||
|
||||
Checking a repo's integrity and consistency
|
||||
===========================================
|
||||
Checking integrity and consistency
|
||||
==================================
|
||||
|
||||
Imagine your repository is saved on a server that has a faulty hard
|
||||
drive, or even worse, attackers get privileged access and modify your
|
||||
backup with the intention to make you restore malicious data:
|
||||
drive, or even worse, attackers get privileged access and modify the
|
||||
files in your repository with the intention to make you restore
|
||||
malicious data:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ echo "boom" >> backup/index/d795ffa99a8ab8f8e42cec1f814df4e48b8f49129360fb57613df93739faee97
|
||||
$ echo "boom" > /srv/restic-repo/index/de30f3231ca2e6a59af4aa84216dfe2ef7339c549dc11b09b84000997b139628
|
||||
|
||||
In order to detect these things, it is a good idea to regularly use the
|
||||
``check`` command to test whether everything is alright, your precious
|
||||
backup data is consistent and the integrity is unharmed:
|
||||
Trying to restore a snapshot which has been modified as shown above
|
||||
will yield an error:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /srv/restic-repo --no-cache restore c23e491f --target /tmp/restore-work
|
||||
...
|
||||
Fatal: unable to load index de30f323: load <index/de30f3231c>: invalid data returned
|
||||
|
||||
In order to detect these things before they become a problem, it's a
|
||||
good idea to regularly use the ``check`` command to test whether your
|
||||
repository is healthy and consistent, and that your precious backup
|
||||
data is unharmed. There are two types of checks that can be performed:
|
||||
|
||||
- Structural consistency and integrity, e.g. snapshots, trees and pack files (default)
|
||||
- Integrity of the actual data that you backed up (enabled with flags, see below)
|
||||
|
||||
To verify the structure of the repository, issue the ``check`` command.
|
||||
If the repository is damaged like in the example above, ``check`` will
|
||||
detect this and yield the same error as when you tried to restore:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /srv/restic-repo check
|
||||
Load indexes
|
||||
ciphertext verification failed
|
||||
...
|
||||
load indexes
|
||||
error: error loading index de30f323: load <index/de30f3231c>: invalid data returned
|
||||
Fatal: LoadIndex returned errors
|
||||
|
||||
Trying to restore a snapshot which has been modified as shown above will
|
||||
yield the same error:
|
||||
If the repository structure is intact, restic will show that no errors were found:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /srv/restic-repo restore 79766175 --target /tmp/restore-work
|
||||
Load indexes
|
||||
ciphertext verification failed
|
||||
$ restic -r /src/restic-repo check
|
||||
...
|
||||
load indexes
|
||||
check all packs
|
||||
check snapshots, trees and blobs
|
||||
no errors were found
|
||||
|
||||
By default, ``check`` command does not check that repository data files
|
||||
are unmodified. Use ``--read-data`` parameter to check all repository
|
||||
data files:
|
||||
By default, the ``check`` command does not verify that the actual data files
|
||||
on disk in the repository are unmodified, because doing so requires reading
|
||||
a copy of every data file in the repository. To tell restic to also verify the
|
||||
integrity of the data files in the repository, use the ``--read-data`` flag:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /srv/restic-repo check --read-data
|
||||
...
|
||||
load indexes
|
||||
check all packs
|
||||
check snapshots, trees and blobs
|
||||
read all data
|
||||
[0:00] 100.00% 3 / 3 items
|
||||
duration: 0:00
|
||||
no errors were found
|
||||
|
||||
Use the ``--read-data-subset=n/t`` parameter to check only a subset of the
|
||||
repository data files at a time. The parameter takes two values, ``n`` and
|
||||
``t``. When the check command runs, all data files in the repository are
|
||||
logically divided in ``t`` (roughly equal) groups, and only files that
|
||||
belong to the group number ``n`` are checked. For example, the following
|
||||
commands check all repository data files over 5 separate invocations:
|
||||
.. note:: Since ``--read-data`` has to download all data files in the
|
||||
repository, beware that it might incur higher bandwidth costs than usual
|
||||
and also that it takes more time than the default ``check``.
|
||||
|
||||
Alternatively, use the ``--read-data-subset=n/t`` parameter to check only a
|
||||
subset of the repository data files at a time. The parameter takes two values,
|
||||
``n`` and ``t``. When the check command runs, all data files in the repository
|
||||
are logically divided in ``t`` (roughly equal) groups, and only files that
|
||||
belong to group number ``n`` are checked. For example, the following commands
|
||||
check all repository data files over 5 separate invocations:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
@@ -138,4 +169,3 @@ commands check all repository data files over 5 separate invocations:
|
||||
$ restic -r /srv/restic-repo check --read-data-subset=3/5
|
||||
$ restic -r /srv/restic-repo check --read-data-subset=4/5
|
||||
$ restic -r /srv/restic-repo check --read-data-subset=5/5
|
||||
|
||||
|
||||
@@ -131,6 +131,6 @@ output the contents in the tar format:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /srv/restic-repo dump /home/other/work latest > restore.tar
|
||||
$ restic -r /srv/restic-repo dump latest /home/other/work > restore.tar
|
||||
|
||||
|
||||
|
||||
@@ -213,12 +213,66 @@ All snapshots are evaluated against all matching ``--keep-*`` counts. A
|
||||
single snapshot on 2017-09-30 (Sat) will count as a daily, weekly and monthly.
|
||||
|
||||
Let's explain this with an example: Suppose you have only made a backup
|
||||
on each Sunday for 12 weeks. Then ``forget --keep-daily 4`` will keep
|
||||
the last four snapshots for the last four Sundays, but remove the rest.
|
||||
Only counting the days which have a backup and ignore the ones without
|
||||
is a safety feature: it prevents restic from removing many snapshots
|
||||
when no new ones are created. If it was implemented otherwise, running
|
||||
``forget --keep-daily 4`` on a Friday would remove all snapshots!
|
||||
on each Sunday for 12 weeks:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic snapshots
|
||||
repository f00c6e2a opened successfully, password is correct
|
||||
ID Time Host Tags Paths
|
||||
---------------------------------------------------------------
|
||||
0a1f9759 2019-09-01 11:00:00 mopped /home/user/work
|
||||
46cfe4d5 2019-09-08 11:00:00 mopped /home/user/work
|
||||
f6b1f037 2019-09-15 11:00:00 mopped /home/user/work
|
||||
eb430a5d 2019-09-22 11:00:00 mopped /home/user/work
|
||||
8cf1cb9a 2019-09-29 11:00:00 mopped /home/user/work
|
||||
5d33b116 2019-10-06 11:00:00 mopped /home/user/work
|
||||
b9553125 2019-10-13 11:00:00 mopped /home/user/work
|
||||
e1a7b58b 2019-10-20 11:00:00 mopped /home/user/work
|
||||
8f8018c0 2019-10-27 11:00:00 mopped /home/user/work
|
||||
59403279 2019-11-03 11:00:00 mopped /home/user/work
|
||||
dfee9fb4 2019-11-10 11:00:00 mopped /home/user/work
|
||||
e1ae2f40 2019-11-17 11:00:00 mopped /home/user/work
|
||||
---------------------------------------------------------------
|
||||
12 snapshots
|
||||
|
||||
Then ``forget --keep-daily 4`` will keep the last four snapshots for the last
|
||||
four Sundays, but remove the rest:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic forget --keep-daily 4 --dry-run
|
||||
repository f00c6e2a opened successfully, password is correct
|
||||
Applying Policy: keep the last 4 daily snapshots
|
||||
keep 4 snapshots:
|
||||
ID Time Host Tags Reasons Paths
|
||||
-------------------------------------------------------------------------------
|
||||
8f8018c0 2019-10-27 11:00:00 mopped daily snapshot /home/user/work
|
||||
59403279 2019-11-03 11:00:00 mopped daily snapshot /home/user/work
|
||||
dfee9fb4 2019-11-10 11:00:00 mopped daily snapshot /home/user/work
|
||||
e1ae2f40 2019-11-17 11:00:00 mopped daily snapshot /home/user/work
|
||||
-------------------------------------------------------------------------------
|
||||
4 snapshots
|
||||
|
||||
remove 8 snapshots:
|
||||
ID Time Host Tags Paths
|
||||
---------------------------------------------------------------
|
||||
0a1f9759 2019-09-01 11:00:00 mopped /home/user/work
|
||||
46cfe4d5 2019-09-08 11:00:00 mopped /home/user/work
|
||||
f6b1f037 2019-09-15 11:00:00 mopped /home/user/work
|
||||
eb430a5d 2019-09-22 11:00:00 mopped /home/user/work
|
||||
8cf1cb9a 2019-09-29 11:00:00 mopped /home/user/work
|
||||
5d33b116 2019-10-06 11:00:00 mopped /home/user/work
|
||||
b9553125 2019-10-13 11:00:00 mopped /home/user/work
|
||||
e1a7b58b 2019-10-20 11:00:00 mopped /home/user/work
|
||||
---------------------------------------------------------------
|
||||
8 snapshots
|
||||
|
||||
The result of the ``forget --keep-daily`` operation does not depend on when it
|
||||
is run, it will only count the days for which a snapshot exists. This is a
|
||||
safety feature: it prevents restic from removing snapshots when no new ones are
|
||||
created. Otherwise, running ``forget --keep-daily 4`` on a Friday (without any
|
||||
snapshot Monday to Thursday) would remove all snapshots!
|
||||
|
||||
Another example: Suppose you make daily backups for 100 years. Then
|
||||
``forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --keep-yearly 75``
|
||||
|
||||
@@ -251,7 +251,7 @@ A snapshot was created and stored in the S3 bucket. By default backups to AWS S3
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./restic backup -o s3.storageclass=REDUCED_REDUNDANCY test.bin
|
||||
$ ./restic backup -o s3.storage-class=REDUCED_REDUNDANCY test.bin
|
||||
|
||||
This snapshot may now be restored:
|
||||
|
||||
|
||||
@@ -22,9 +22,7 @@ The program can be built with debug support like this:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ go run build.go -mod=vendor -tags debug
|
||||
|
||||
For Go < 1.11, the option ``-mod=vendor`` needs to be removed.
|
||||
$ go run build.go -tags debug
|
||||
|
||||
Afterwards, extensive debug messages are written to the file in
|
||||
environment variable ``DEBUG_LOG``, e.g.:
|
||||
|
||||
BIN
doc/_static/favicon.ico
vendored
BIN
doc/_static/favicon.ico
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -1026,8 +1026,6 @@ _restic_mount()
|
||||
|
||||
flags+=("--allow-other")
|
||||
local_nonpersistent_flags+=("--allow-other")
|
||||
flags+=("--allow-root")
|
||||
local_nonpersistent_flags+=("--allow-root")
|
||||
flags+=("--help")
|
||||
flags+=("-h")
|
||||
local_nonpersistent_flags+=("--help")
|
||||
|
||||
@@ -47,10 +47,10 @@ In the following example, we'll use the file ``restic-0.9.3.tar.gz`` and Go
|
||||
$ go version
|
||||
go version go1.11.1 linux/amd64
|
||||
|
||||
$ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor -ldflags "-s -w" -tags selfupdate -o restic_linux_amd64 ./cmd/restic
|
||||
$ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -tags selfupdate -o restic_linux_amd64 ./cmd/restic
|
||||
$ bzip2 restic_linux_amd64
|
||||
|
||||
$ GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor -ldflags "-s -w" -tags selfupdate -o restic_windows_amd64.exe ./cmd/restic
|
||||
$ GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -tags selfupdate -o restic_windows_amd64.exe ./cmd/restic
|
||||
$ touch --reference VERSION restic_windows_amd64.exe
|
||||
$ TZ=Europe/Berlin zip -q -X restic_windows_amd64.zip restic_windows_amd64.exe
|
||||
|
||||
@@ -105,7 +105,7 @@ The following steps are necessary to build the binaries:
|
||||
--volume "$PWD/restic-0.9.3:/restic" \
|
||||
--volume "$PWD/output:/output" \
|
||||
restic/builder \
|
||||
go run -mod=vendor helpers/build-release-binaries/main.go --verbose
|
||||
go run helpers/build-release-binaries/main.go --verbose
|
||||
|
||||
Prepare a New Release
|
||||
*********************
|
||||
@@ -118,6 +118,6 @@ required argument is the new version number (in `Semantic Versioning
|
||||
|
||||
.. code::
|
||||
|
||||
go run -mod=vendor helpers/prepare-release/main.go 0.9.3
|
||||
go run helpers/prepare-release/main.go 0.9.3
|
||||
|
||||
Checks can be skipped on demand via flags, please see ``--help`` for details.
|
||||
|
||||
14
doc/faq.rst
14
doc/faq.rst
@@ -142,6 +142,9 @@ is not slowed down, which is particularly useful for servers.
|
||||
Creating new repo on a Synology NAS via sftp fails
|
||||
--------------------------------------------------
|
||||
|
||||
For using restic with a Synology NAS via sftp, please make sure that the
|
||||
specified path is absolute, it must start with a slash (``/``).
|
||||
|
||||
Sometimes creating a new restic repository on a Synology NAS via sftp fails
|
||||
with an error similar to the following:
|
||||
|
||||
@@ -160,8 +163,8 @@ different than the directory structure on the device and maybe even as exposed
|
||||
via other protocols.
|
||||
|
||||
|
||||
Try removing the /volume1 prefix in your paths. If this does not work, use sftp
|
||||
and ls to explore the SFTP file system hierarchy on your NAS.
|
||||
Try removing the ``/volume1`` prefix in your paths. If this does not work, use
|
||||
sftp and ls to explore the SFTP file system hierarchy on your NAS.
|
||||
|
||||
The following may work:
|
||||
|
||||
@@ -172,4 +175,9 @@ The following may work:
|
||||
Why does restic perform so poorly on Windows?
|
||||
---------------------------------------------
|
||||
|
||||
In some cases the real-time protection of antivirus software can interfere with restic's operations. If you are experiencing bad performance you can try to temporarily disable your antivirus software to find out if it is the cause for your performance problems.
|
||||
In some cases the real-time protection of antivirus software can interfere with
|
||||
restic's operations. If you are experiencing bad performance you can try to
|
||||
temporarily disable your antivirus software to find out if it is the cause for
|
||||
your performance problems. If you are certain that the antivirus software is
|
||||
the cause for this and you want to gain maximum performance, you have to add
|
||||
the restic binary to an exclusions list within the antivirus software.
|
||||
|
||||
@@ -56,10 +56,6 @@ For details please see the documentation for time.Format() at:
|
||||
\fB\-\-allow\-other\fP[=false]
|
||||
allow other users to access the data in the mounted directory
|
||||
|
||||
.PP
|
||||
\fB\-\-allow\-root\fP[=false]
|
||||
allow root user to access the data in the mounted directory
|
||||
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for mount
|
||||
|
||||
@@ -22,7 +22,7 @@ Usage help is available:
|
||||
check Check the repository for errors
|
||||
diff Show differences between two snapshots
|
||||
dump Print a backed-up file to stdout
|
||||
find Find a file or directory
|
||||
find Find a file, a directory or restic IDs
|
||||
forget Remove snapshots from the repository
|
||||
generate Generate manual pages and auto-completion files (bash, zsh)
|
||||
help Help about any command
|
||||
@@ -34,30 +34,33 @@ Usage help is available:
|
||||
mount Mount the repository
|
||||
prune Remove unneeded data from the repository
|
||||
rebuild-index Build a new index file
|
||||
recover Recover data from the repository
|
||||
restore Extract the data from a snapshot
|
||||
self-update Update the restic binary
|
||||
snapshots List all snapshots
|
||||
stats Count up sizes and show information about repository data
|
||||
stats Scan the repository and show basic statistics
|
||||
tag Modify tags on snapshots
|
||||
unlock Remove locks other processes created
|
||||
version Print version information
|
||||
|
||||
Flags:
|
||||
--cacert file file to load root certificates from (default: use system certificates)
|
||||
--cache-dir string set the cache directory. (default: use system default cache directory)
|
||||
--cleanup-cache auto remove old cache directories
|
||||
-h, --help help for restic
|
||||
--json set output mode to JSON for commands that support it
|
||||
--key-hint string key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)
|
||||
--limit-download int limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
--limit-upload int limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
--no-cache do not use a local cache
|
||||
--no-lock do not lock the repo, this allows some operations on read-only repos
|
||||
-o, --option key=value set extended option (key=value, can be specified multiple times)
|
||||
-p, --password-file string read the repository password from a file (default: $RESTIC_PASSWORD_FILE)
|
||||
-q, --quiet do not output comprehensive progress report
|
||||
-r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY)
|
||||
--tls-client-cert string path to a file containing PEM encoded TLS client certificate and private key
|
||||
-v, --verbose n[=-1] be verbose (specify --verbose multiple times or level n)
|
||||
--cacert file file to load root certificates from (default: use system certificates)
|
||||
--cache-dir directory set the cache directory. (default: use system default cache directory)
|
||||
--cleanup-cache auto remove old cache directories
|
||||
-h, --help help for restic
|
||||
--json set output mode to JSON for commands that support it
|
||||
--key-hint key key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)
|
||||
--limit-download int limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
--limit-upload int limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
--no-cache do not use a local cache
|
||||
--no-lock do not lock the repo, this allows some operations on read-only repos
|
||||
-o, --option key=value set extended option (key=value, can be specified multiple times)
|
||||
--password-command command specify a shell command to obtain a password (default: $RESTIC_PASSWORD_COMMAND)
|
||||
-p, --password-file file read the repository password from a file (default: $RESTIC_PASSWORD_FILE)
|
||||
-q, --quiet do not output comprehensive progress report
|
||||
-r, --repo repository repository to backup to or restore from (default: $RESTIC_REPOSITORY)
|
||||
--tls-client-cert file path to a file containing PEM encoded TLS client certificate and private key
|
||||
-v, --verbose n be verbose (specify --verbose multiple times or level n)
|
||||
|
||||
Use "restic [command] --help" for more information about a command.
|
||||
|
||||
@@ -73,42 +76,53 @@ command:
|
||||
The "backup" command creates a new snapshot and saves the files and directories
|
||||
given as the arguments.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
|
||||
Note that some issues such as unreadable or deleted files during backup
|
||||
currently doesn't result in a non-zero error exit status.
|
||||
|
||||
Usage:
|
||||
restic backup [flags] FILE/DIR [FILE/DIR] ...
|
||||
|
||||
Flags:
|
||||
-e, --exclude pattern exclude a pattern (can be specified multiple times)
|
||||
--exclude-caches excludes cache directories that are marked with a CACHEDIR.TAG file. See http://bford.info/cachedir/spec.html for the Cache Directory Tagging Standard
|
||||
--exclude-file file read exclude patterns from a file (can be specified multiple times)
|
||||
--exclude-if-present stringArray 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)
|
||||
--files-from string read the files to backup from file (can be combined with file args/can be specified multiple times)
|
||||
-f, --force force re-reading the target files/directories (overrides the "parent" flag)
|
||||
-h, --help help for backup
|
||||
--hostname hostname set the hostname for the snapshot manually. To prevent an expensive rescan use the "parent" flag
|
||||
-x, --one-file-system exclude other file systems
|
||||
--parent string use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)
|
||||
--stdin read backup from stdin
|
||||
--stdin-filename string file name to use when reading from stdin (default "stdin")
|
||||
--tag tag add a tag for the new snapshot (can be specified multiple times)
|
||||
--time string time of the backup (ex. '2012-11-01 22:08:41') (default: now)
|
||||
--with-atime store the atime for all files and directories
|
||||
-e, --exclude pattern exclude a pattern (can be specified multiple times)
|
||||
--exclude-caches excludes cache directories that are marked with a CACHEDIR.TAG file. See http://bford.info/cachedir/spec.html for the Cache Directory Tagging Standard
|
||||
--exclude-file file read exclude patterns from a file (can be specified multiple times)
|
||||
--exclude-if-present filename[:header] 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)
|
||||
--files-from file read the files to backup from file (can be combined with file args/can be specified multiple times)
|
||||
-f, --force force re-reading the target files/directories (overrides the "parent" flag)
|
||||
-h, --help help for backup
|
||||
-H, --host hostname set the hostname for the snapshot manually. To prevent an expensive rescan use the "parent" flag
|
||||
--iexclude pattern same as --exclude pattern but ignores the casing of filenames
|
||||
--ignore-inode ignore inode number changes when checking for modified files
|
||||
-x, --one-file-system exclude other file systems
|
||||
--parent snapshot use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)
|
||||
--stdin read backup from stdin
|
||||
--stdin-filename filename filename to use when reading from stdin (default "stdin")
|
||||
--tag tag add a tag for the new snapshot (can be specified multiple times)
|
||||
--time time time of the backup (ex. '2012-11-01 22:08:41') (default: now)
|
||||
--with-atime store the atime for all files and directories
|
||||
|
||||
Global Flags:
|
||||
--cacert file file to load root certificates from (default: use system certificates)
|
||||
--cache-dir string set the cache directory. (default: use system default cache directory)
|
||||
--cleanup-cache auto remove old cache directories
|
||||
--json set output mode to JSON for commands that support it
|
||||
--key-hint string key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)
|
||||
--limit-download int limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
--limit-upload int limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
--no-cache do not use a local cache
|
||||
--no-lock do not lock the repo, this allows some operations on read-only repos
|
||||
-o, --option key=value set extended option (key=value, can be specified multiple times)
|
||||
-p, --password-file string read the repository password from a file (default: $RESTIC_PASSWORD_FILE)
|
||||
-q, --quiet do not output comprehensive progress report
|
||||
-r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY)
|
||||
--tls-client-cert string path to a file containing PEM encoded TLS client certificate and private key
|
||||
-v, --verbose n[=-1] be verbose (specify --verbose multiple times or level n)
|
||||
--cacert file file to load root certificates from (default: use system certificates)
|
||||
--cache-dir directory set the cache directory. (default: use system default cache directory)
|
||||
--cleanup-cache auto remove old cache directories
|
||||
--json set output mode to JSON for commands that support it
|
||||
--key-hint key key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)
|
||||
--limit-download int limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
--limit-upload int limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
--no-cache do not use a local cache
|
||||
--no-lock do not lock the repo, this allows some operations on read-only repos
|
||||
-o, --option key=value set extended option (key=value, can be specified multiple times)
|
||||
--password-command command specify a shell command to obtain a password (default: $RESTIC_PASSWORD_COMMAND)
|
||||
-p, --password-file file read the repository password from a file (default: $RESTIC_PASSWORD_FILE)
|
||||
-q, --quiet do not output comprehensive progress report
|
||||
-r, --repo repository repository to backup to or restore from (default: $RESTIC_REPOSITORY)
|
||||
--tls-client-cert file path to a file containing PEM encoded TLS client certificate and private key
|
||||
-v, --verbose n be verbose (specify --verbose multiple times or level n)
|
||||
|
||||
Subcommand that support showing progress information such as ``backup``,
|
||||
``check`` and ``prune`` will do so unless the quiet flag ``-q`` or
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
echo "Build binary using golang docker image"
|
||||
docker run --rm -ti \
|
||||
-v "`pwd`":/go/src/github.com/restic/restic \
|
||||
-w /go/src/github.com/restic/restic golang:1.11.1-alpine go run build.go
|
||||
-w /go/src/github.com/restic/restic golang:1.13.6-alpine go run build.go
|
||||
|
||||
echo "Build docker image restic/restic:latest"
|
||||
docker build --rm -t restic/restic:latest -f docker/Dockerfile .
|
||||
|
||||
10
go.mod
10
go.mod
@@ -1,11 +1,12 @@
|
||||
module github.com/restic/restic
|
||||
|
||||
require (
|
||||
bazil.org/fuse v0.0.0-20180421153158-65cc252bf669
|
||||
bazil.org/fuse v0.0.0-20191225072544-27e78e7d88df
|
||||
cloud.google.com/go v0.37.4 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v27.3.0+incompatible
|
||||
github.com/Azure/go-autorest/autorest v0.9.2 // indirect
|
||||
github.com/cenkalti/backoff v2.1.1+incompatible
|
||||
github.com/cespare/xxhash v1.1.0
|
||||
github.com/cpuguy83/go-md2man v1.0.10 // indirect
|
||||
github.com/dnaeon/go-vcr v1.0.1 // indirect
|
||||
github.com/elithrar/simple-scrypt v1.3.0
|
||||
@@ -19,14 +20,13 @@ require (
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/kurin/blazer v0.5.3
|
||||
github.com/marstr/guid v1.1.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.7
|
||||
github.com/minio/minio-go/v6 v6.0.43
|
||||
github.com/ncw/swift v1.0.47
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pkg/profile v1.3.0
|
||||
github.com/pkg/sftp v1.10.0
|
||||
github.com/pkg/xattr v0.4.1
|
||||
github.com/restic/chunker v0.2.0
|
||||
github.com/restic/chunker v0.3.0
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
|
||||
github.com/spf13/cobra v0.0.3
|
||||
@@ -37,7 +37,7 @@ require (
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2
|
||||
google.golang.org/api v0.3.2
|
||||
google.golang.org/appengine v1.5.0 // indirect
|
||||
@@ -49,3 +49,5 @@ require (
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
||||
replace github.com/restic/chunker => ./chunker
|
||||
|
||||
21
go.sum
21
go.sum
@@ -1,5 +1,5 @@
|
||||
bazil.org/fuse v0.0.0-20180421153158-65cc252bf669 h1:FNCRpXiquG1aoyqcIWVFmpTSKVcx2bQD38uZZeGtdlw=
|
||||
bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
|
||||
bazil.org/fuse v0.0.0-20191225072544-27e78e7d88df h1:Jse+TREHl28kVQHa2qqDSPztzyr9UlIph013BRGYmVQ=
|
||||
bazil.org/fuse v0.0.0-20191225072544-27e78e7d88df/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
|
||||
@@ -20,6 +20,8 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L
|
||||
github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
|
||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -28,6 +30,8 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
|
||||
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
@@ -96,8 +100,6 @@ github.com/kurin/blazer v0.5.3 h1:SAgYv0TKU0kN/ETfO5ExjNAPyMt2FocO2s/UlCHfjAk=
|
||||
github.com/kurin/blazer v0.5.3/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU=
|
||||
github.com/marstr/guid v1.1.0 h1:/M4H/1G4avsieL6BbUwCOBzulmoeKVP5ux/3mQNnbyI=
|
||||
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
|
||||
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/minio/minio-go/v6 v6.0.43 h1:D7c6Kx0ZB5U8EXJ6SQVOqPzapaLK/qpxQIktCnPHp/o=
|
||||
github.com/minio/minio-go/v6 v6.0.43/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg=
|
||||
@@ -132,8 +134,8 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/restic/chunker v0.2.0 h1:GjvmvFuv2mx0iekZs+iAlrioo2UtgsGSSplvoXaVHDU=
|
||||
github.com/restic/chunker v0.2.0/go.mod h1:VdjruEj+7BU1ZZTW8Qqi1exxRx2Omf2JH0NsUEkQ29s=
|
||||
github.com/restic/chunker v0.3.0 h1:8OGNG5ALPTmHTdfuNkwqHqbzifrIc3MeL8CL7q9BY34=
|
||||
github.com/restic/chunker v0.3.0/go.mod h1:VdjruEj+7BU1ZZTW8Qqi1exxRx2Omf2JH0NsUEkQ29s=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
@@ -145,6 +147,8 @@ github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHah
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
@@ -155,6 +159,8 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
|
||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2 h1:NAfh7zF0/3/HqtMvJNZ/RFrSlCE6ZTlHmKfhL/Dm1Jk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
@@ -199,10 +205,11 @@ golang.org/x/sys v0.0.0-20181021155630-eda9bb28ed51/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
|
||||
|
||||
@@ -96,7 +96,6 @@ func build(sourceDir, outputDir, goos, goarch string) (filename string) {
|
||||
outputFile := filepath.Join(outputDir, filename)
|
||||
|
||||
c := exec.Command("go", "build",
|
||||
"-mod=vendor",
|
||||
"-o", outputFile,
|
||||
"-ldflags", "-s -w",
|
||||
"-tags", "selfupdate",
|
||||
@@ -226,7 +225,7 @@ func buildTargets(sourceDir, outputDir string, targets map[string][]string) {
|
||||
var defaultBuildTargets = map[string][]string{
|
||||
"darwin": []string{"386", "amd64"},
|
||||
"freebsd": []string{"386", "amd64", "arm"},
|
||||
"linux": []string{"386", "amd64", "arm", "arm64"},
|
||||
"linux": []string{"386", "amd64", "arm", "arm64", "ppc64le"},
|
||||
"openbsd": []string{"386", "amd64"},
|
||||
"windows": []string{"386", "amd64"},
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ func runBuild(sourceDir, outputDir, version string) {
|
||||
"--volume", sourceDir+":/restic",
|
||||
"--volume", outputDir+":/output",
|
||||
"restic/builder",
|
||||
"go", "run", "-mod=vendor", "helpers/build-release-binaries/main.go",
|
||||
"go", "run", "helpers/build-release-binaries/main.go",
|
||||
"--version", version)
|
||||
}
|
||||
|
||||
|
||||
@@ -216,10 +216,11 @@ func (arch *Archiver) SaveDir(ctx context.Context, snPath string, fi os.FileInfo
|
||||
return FutureTree{}, err
|
||||
}
|
||||
|
||||
names, err := readdirnames(arch.FS, dir)
|
||||
names, err := readdirnames(arch.FS, dir, fs.O_NOFOLLOW)
|
||||
if err != nil {
|
||||
return FutureTree{}, err
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
nodes := make([]FutureNode, 0, len(names))
|
||||
|
||||
@@ -377,6 +378,7 @@ func (arch *Archiver) Save(ctx context.Context, snPath, target string, previous
|
||||
// make sure it's still a file
|
||||
if !fs.IsRegularFile(fi) {
|
||||
err = errors.Errorf("file %v changed type, refusing to archive")
|
||||
_ = file.Close()
|
||||
err = arch.error(abstarget, fi, err)
|
||||
if err != nil {
|
||||
return FutureNode{}, false, err
|
||||
@@ -514,7 +516,7 @@ func (arch *Archiver) SaveTree(ctx context.Context, snPath string, atree *Tree,
|
||||
for name := range atree.Nodes {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Stable(sort.StringSlice(names))
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
subatree := atree.Nodes[name]
|
||||
@@ -627,43 +629,9 @@ func (arch *Archiver) SaveTree(ctx context.Context, snPath string, atree *Tree,
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
type fileInfoSlice []os.FileInfo
|
||||
|
||||
func (fi fileInfoSlice) Len() int {
|
||||
return len(fi)
|
||||
}
|
||||
|
||||
func (fi fileInfoSlice) Swap(i, j int) {
|
||||
fi[i], fi[j] = fi[j], fi[i]
|
||||
}
|
||||
|
||||
func (fi fileInfoSlice) Less(i, j int) bool {
|
||||
return fi[i].Name() < fi[j].Name()
|
||||
}
|
||||
|
||||
func readdir(filesystem fs.FS, dir string) ([]os.FileInfo, error) {
|
||||
f, err := filesystem.OpenFile(dir, fs.O_RDONLY|fs.O_NOFOLLOW, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Open")
|
||||
}
|
||||
|
||||
entries, err := f.Readdir(-1)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
return nil, errors.Wrapf(err, "Readdir %v failed", dir)
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Sort(fileInfoSlice(entries))
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func readdirnames(filesystem fs.FS, dir string) ([]string, error) {
|
||||
f, err := filesystem.OpenFile(dir, fs.O_RDONLY|fs.O_NOFOLLOW, 0)
|
||||
// flags are passed to fs.OpenFile. O_RDONLY is implied.
|
||||
func readdirnames(filesystem fs.FS, dir string, flags int) ([]string, error) {
|
||||
f, err := filesystem.OpenFile(dir, fs.O_RDONLY|flags, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Open")
|
||||
}
|
||||
@@ -679,32 +647,32 @@ func readdirnames(filesystem fs.FS, dir string) ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Sort(sort.StringSlice(entries))
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// resolveRelativeTargets replaces targets that only contain relative
|
||||
// directories ("." or "../../") with the contents of the directory. Each
|
||||
// element of target is processed with fs.Clean().
|
||||
func resolveRelativeTargets(fs fs.FS, targets []string) ([]string, error) {
|
||||
func resolveRelativeTargets(filesys fs.FS, targets []string) ([]string, error) {
|
||||
debug.Log("targets before resolving: %v", targets)
|
||||
result := make([]string, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
target = fs.Clean(target)
|
||||
pc, _ := pathComponents(fs, target, false)
|
||||
target = filesys.Clean(target)
|
||||
pc, _ := pathComponents(filesys, target, false)
|
||||
if len(pc) > 0 {
|
||||
result = append(result, target)
|
||||
continue
|
||||
}
|
||||
|
||||
debug.Log("replacing %q with readdir(%q)", target, target)
|
||||
entries, err := readdirnames(fs, target)
|
||||
entries, err := readdirnames(filesys, target, fs.O_NOFOLLOW)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(entries)
|
||||
|
||||
for _, name := range entries {
|
||||
result = append(result, fs.Join(target, name))
|
||||
result = append(result, filesys.Join(target, name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,7 +721,6 @@ func (arch *Archiver) runWorkers(ctx context.Context, t *tomb.Tomb) {
|
||||
arch.blobSaver = NewBlobSaver(ctx, t, arch.Repo, arch.Options.SaveBlobConcurrency)
|
||||
|
||||
arch.fileSaver = NewFileSaver(ctx, t,
|
||||
arch.FS,
|
||||
arch.blobSaver.Save,
|
||||
arch.Repo.Config().ChunkerPolynomial,
|
||||
arch.Options.FileReadConcurrency, arch.Options.SaveBlobConcurrency)
|
||||
@@ -822,6 +789,10 @@ func (arch *Archiver) Snapshot(ctx context.Context, targets []string, opts Snaps
|
||||
}
|
||||
|
||||
sn, err := restic.NewSnapshot(targets, opts.Tags, opts.Hostname, opts.Time)
|
||||
if err != nil {
|
||||
return nil, restic.ID{}, err
|
||||
}
|
||||
|
||||
sn.Excludes = opts.Excludes
|
||||
if !opts.ParentSnapshot.IsNull() {
|
||||
id := opts.ParentSnapshot
|
||||
|
||||
@@ -1856,7 +1856,7 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
|
||||
var tests = []struct {
|
||||
src TestDir
|
||||
wantOpen map[string]uint
|
||||
failAfter uint // error after so many files have been saved to the repo
|
||||
failAfter uint // error after so many blobs have been saved to the repo
|
||||
err error
|
||||
}{
|
||||
{
|
||||
@@ -1876,26 +1876,29 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
|
||||
{
|
||||
src: TestDir{
|
||||
"dir": TestDir{
|
||||
"file1": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file2": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file3": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file4": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file5": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file6": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file7": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file8": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file9": TestFile{Content: string(restictest.Random(3, 4*1024*1024))},
|
||||
"file1": TestFile{Content: string(restictest.Random(1, 1024))},
|
||||
"file2": TestFile{Content: string(restictest.Random(2, 1024))},
|
||||
"file3": TestFile{Content: string(restictest.Random(3, 1024))},
|
||||
"file4": TestFile{Content: string(restictest.Random(4, 1024))},
|
||||
"file5": TestFile{Content: string(restictest.Random(5, 1024))},
|
||||
"file6": TestFile{Content: string(restictest.Random(6, 1024))},
|
||||
"file7": TestFile{Content: string(restictest.Random(7, 1024))},
|
||||
"file8": TestFile{Content: string(restictest.Random(8, 1024))},
|
||||
"file9": TestFile{Content: string(restictest.Random(9, 1024))},
|
||||
},
|
||||
},
|
||||
wantOpen: map[string]uint{
|
||||
filepath.FromSlash("dir/file1"): 1,
|
||||
filepath.FromSlash("dir/file2"): 1,
|
||||
filepath.FromSlash("dir/file3"): 1,
|
||||
filepath.FromSlash("dir/file4"): 1,
|
||||
filepath.FromSlash("dir/file7"): 0,
|
||||
filepath.FromSlash("dir/file8"): 0,
|
||||
filepath.FromSlash("dir/file9"): 0,
|
||||
},
|
||||
failAfter: 5,
|
||||
// fails four to six files were opened as the FileReadConcurrency allows for
|
||||
// two queued files
|
||||
failAfter: 4,
|
||||
err: testErr,
|
||||
},
|
||||
}
|
||||
@@ -1926,7 +1929,10 @@ func TestArchiverAbortEarlyOnError(t *testing.T) {
|
||||
err: test.err,
|
||||
}
|
||||
|
||||
arch := New(testRepo, testFS, Options{})
|
||||
// at most two files may be queued
|
||||
arch := New(testRepo, testFS, Options{
|
||||
FileReadConcurrency: 2,
|
||||
})
|
||||
|
||||
_, _, err := arch.Snapshot(ctx, []string{"."}, SnapshotOptions{Time: time.Now()})
|
||||
if errors.Cause(err) != test.err {
|
||||
@@ -1985,19 +1991,22 @@ func chmod(t testing.TB, filename string, mode os.FileMode) {
|
||||
type StatFS struct {
|
||||
fs.FS
|
||||
|
||||
OverrideLstat map[string]os.FileInfo
|
||||
OverrideLstat map[string]os.FileInfo
|
||||
OnlyOverrideStat bool
|
||||
}
|
||||
|
||||
func (fs *StatFS) Lstat(name string) (os.FileInfo, error) {
|
||||
if fi, ok := fs.OverrideLstat[name]; ok {
|
||||
return fi, nil
|
||||
if !fs.OnlyOverrideStat {
|
||||
if fi, ok := fs.OverrideLstat[fixpath(name)]; ok {
|
||||
return fi, nil
|
||||
}
|
||||
}
|
||||
|
||||
return fs.FS.Lstat(name)
|
||||
}
|
||||
|
||||
func (fs *StatFS) OpenFile(name string, flags int, perm os.FileMode) (fs.File, error) {
|
||||
if fi, ok := fs.OverrideLstat[name]; ok {
|
||||
if fi, ok := fs.OverrideLstat[fixpath(name)]; ok {
|
||||
f, err := fs.FS.OpenFile(name, flags, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2062,7 +2071,11 @@ func TestMetadataChanged(t *testing.T) {
|
||||
// set some values so we can then compare the nodes
|
||||
want.Content = node2.Content
|
||||
want.Path = ""
|
||||
want.ExtendedAttributes = nil
|
||||
if len(want.ExtendedAttributes) == 0 {
|
||||
want.ExtendedAttributes = nil
|
||||
}
|
||||
|
||||
want.AccessTime = want.ModTime
|
||||
|
||||
// make sure that metadata was recorded successfully
|
||||
if !cmp.Equal(want, node2) {
|
||||
@@ -2085,6 +2098,10 @@ func TestMetadataChanged(t *testing.T) {
|
||||
|
||||
// make another snapshot
|
||||
snapshotID, node3 := snapshot(t, repo, fs, snapshotID, "testfile")
|
||||
// Override username and group to empty string - in case underlying system has user with UID 51234
|
||||
// See https://github.com/restic/restic/issues/2372
|
||||
node3.User = ""
|
||||
node3.Group = ""
|
||||
|
||||
// make sure that metadata was recorded successfully
|
||||
if !cmp.Equal(want, node3) {
|
||||
@@ -2096,3 +2113,51 @@ func TestMetadataChanged(t *testing.T) {
|
||||
|
||||
checker.TestCheckRepo(t, repo)
|
||||
}
|
||||
|
||||
func TestRacyFileSwap(t *testing.T) {
|
||||
files := TestDir{
|
||||
"file": TestFile{
|
||||
Content: "foo bar test file",
|
||||
},
|
||||
}
|
||||
|
||||
tempdir, repo, cleanup := prepareTempdirRepoSrc(t, files)
|
||||
defer cleanup()
|
||||
|
||||
back := fs.TestChdir(t, tempdir)
|
||||
defer back()
|
||||
|
||||
// get metadata of current folder
|
||||
fi := lstat(t, ".")
|
||||
tempfile := filepath.Join(tempdir, "file")
|
||||
|
||||
statfs := &StatFS{
|
||||
FS: fs.Local{},
|
||||
OverrideLstat: map[string]os.FileInfo{
|
||||
tempfile: fi,
|
||||
},
|
||||
OnlyOverrideStat: true,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var tmb tomb.Tomb
|
||||
|
||||
arch := New(repo, fs.Track{FS: statfs}, Options{})
|
||||
arch.Error = func(item string, fi os.FileInfo, err error) error {
|
||||
t.Logf("archiver error as expected for %v: %v", item, err)
|
||||
return err
|
||||
}
|
||||
arch.runWorkers(tmb.Context(ctx), &tmb)
|
||||
|
||||
// fs.Track will panic if the file was not closed
|
||||
_, excluded, err := arch.Save(ctx, "/", tempfile, nil)
|
||||
if err == nil {
|
||||
t.Errorf("Save() should have failed")
|
||||
}
|
||||
|
||||
if excluded {
|
||||
t.Errorf("Save() excluded the node, that's unexpected")
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user