mirror of
https://github.com/restic/restic.git
synced 2025-08-25 06:37:28 +00:00
Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7d4b7ad9cb | ||
![]() |
a883bb6596 | ||
![]() |
91acef90b2 | ||
![]() |
b2b7727b31 | ||
![]() |
0e4c9a5421 | ||
![]() |
49fa8fe6dd | ||
![]() |
12f167ee79 | ||
![]() |
bb018fbc3e | ||
![]() |
3b24e0ac55 | ||
![]() |
04da31af2b | ||
![]() |
65923e9c26 | ||
![]() |
b903081804 | ||
![]() |
beb1e872cc | ||
![]() |
db350c0430 | ||
![]() |
716a5dd20d | ||
![]() |
dbd07ade98 | ||
![]() |
7adf1e5d37 | ||
![]() |
8f94eb5420 | ||
![]() |
8aaba83719 | ||
![]() |
e16a6d4c50 | ||
![]() |
34e67e3510 | ||
![]() |
c527c05590 | ||
![]() |
ed23edeb62 | ||
![]() |
0f398b82e3 | ||
![]() |
99755c634b | ||
![]() |
f5f13f6648 | ||
![]() |
00216d54a1 | ||
![]() |
1f3f68b2c0 | ||
![]() |
57acc769b4 | ||
![]() |
20ad14e362 | ||
![]() |
c995b5be52 | ||
![]() |
1adf28a2b5 | ||
![]() |
6d9675c323 | ||
![]() |
551b31ce3c | ||
![]() |
ec99507e4c | ||
![]() |
5f97f534b1 | ||
![]() |
ed11bbd0e2 | ||
![]() |
5bb9cb056d | ||
![]() |
cd9bd22563 | ||
![]() |
ecc826ef7d | ||
![]() |
fb43cbab49 | ||
![]() |
41d31b1e27 | ||
![]() |
f6ea5c5865 | ||
![]() |
4a7a6b06af | ||
![]() |
e499bbe3ae | ||
![]() |
52682b1c7b | ||
![]() |
c15b4bceae | ||
![]() |
74348be3fa | ||
![]() |
72922a79ed |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -4,10 +4,10 @@ updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
|
||||
# Dependencies listed in .github/workflows/*.yml
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
|
75
CHANGELOG.md
75
CHANGELOG.md
@@ -1,3 +1,78 @@
|
||||
Changelog for restic 0.15.1 (2023-01-30)
|
||||
=======================================
|
||||
|
||||
The following sections list the changes in restic 0.15.1 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
* Fix #3750: Remove `b2_download_file_by_name: 404` warning from B2 backend
|
||||
* Fix #4147: Make `prune --quiet` not print progress bar
|
||||
* Fix #4163: Make `self-update --output` work with new filename on Windows
|
||||
* Fix #4167: Add missing ETA in `backup` progress bar
|
||||
* Enh #4143: Ignore empty lock files
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
* Bugfix #3750: Remove `b2_download_file_by_name: 404` warning from B2 backend
|
||||
|
||||
In some cases the B2 backend could print `b2_download_file_by_name: 404: : b2.b2err`
|
||||
warnings. These are only debug messages and can be safely ignored.
|
||||
|
||||
Restic now uses an updated library for accessing B2, which removes the warning.
|
||||
|
||||
https://github.com/restic/restic/issues/3750
|
||||
https://github.com/restic/restic/issues/4144
|
||||
https://github.com/restic/restic/pull/4146
|
||||
|
||||
* Bugfix #4147: Make `prune --quiet` not print progress bar
|
||||
|
||||
A regression in restic 0.15.0 caused `prune --quiet` to show a progress bar while deciding how
|
||||
to process each pack files. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/4147
|
||||
https://github.com/restic/restic/pull/4153
|
||||
|
||||
* Bugfix #4163: Make `self-update --output` work with new filename on Windows
|
||||
|
||||
Since restic 0.14.0 the `self-update` command did not work when a custom output filename was
|
||||
specified via the `--output` option. This has now been fixed.
|
||||
|
||||
As a workaround, either use an older restic version to run the self-update or create an empty
|
||||
file with the output filename before updating e.g. using CMD:
|
||||
|
||||
`type nul > new-file.exe` `restic self-update --output new-file.exe`
|
||||
|
||||
https://github.com/restic/restic/pull/4163
|
||||
https://forum.restic.net/t/self-update-windows-started-failing-after-release-of-0-15/5836
|
||||
|
||||
* Bugfix #4167: Add missing ETA in `backup` progress bar
|
||||
|
||||
A regression in restic 0.15.0 caused the ETA to be missing from the progress bar displayed by the
|
||||
`backup` command. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/pull/4167
|
||||
|
||||
* Enhancement #4143: Ignore empty lock files
|
||||
|
||||
With restic 0.15.0 the checks for stale locks became much stricter than before. In particular,
|
||||
empty or unreadable locks were no longer silently ignored. This made restic to complain with
|
||||
`Load(<lock/1234567812>, 0, 0) returned error, retrying after 552.330144ms:
|
||||
load(<lock/1234567812>): invalid data returned` and fail in the end.
|
||||
|
||||
The error message is now clarified and the implementation changed to ignore empty lock files
|
||||
which are sometimes created as the result of a failed uploads on some backends.
|
||||
|
||||
Please note that unreadable lock files still have to cleaned up manually. To do so, you can run
|
||||
`restic unlock --remove-all` which removes all existing lock files. But first make sure that
|
||||
no other restic process is currently using the repository.
|
||||
|
||||
https://github.com/restic/restic/issues/4143
|
||||
https://github.com/restic/restic/pull/4152
|
||||
|
||||
|
||||
Changelog for restic 0.15.0 (2023-01-12)
|
||||
=======================================
|
||||
|
||||
|
10
changelog/0.15.1_2023-01-30/issue-3750
Normal file
10
changelog/0.15.1_2023-01-30/issue-3750
Normal file
@@ -0,0 +1,10 @@
|
||||
Bugfix: Remove `b2_download_file_by_name: 404` warning from B2 backend
|
||||
|
||||
In some cases the B2 backend could print `b2_download_file_by_name: 404: :
|
||||
b2.b2err` warnings. These are only debug messages and can be safely ignored.
|
||||
|
||||
Restic now uses an updated library for accessing B2, which removes the warning.
|
||||
|
||||
https://github.com/restic/restic/issues/3750
|
||||
https://github.com/restic/restic/issues/4144
|
||||
https://github.com/restic/restic/pull/4146
|
7
changelog/0.15.1_2023-01-30/issue-4147
Normal file
7
changelog/0.15.1_2023-01-30/issue-4147
Normal file
@@ -0,0 +1,7 @@
|
||||
Bugfix: Make `prune --quiet` not print progress bar
|
||||
|
||||
A regression in restic 0.15.0 caused `prune --quiet` to show a progress bar
|
||||
while deciding how to process each pack files. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/4147
|
||||
https://github.com/restic/restic/pull/4153
|
19
changelog/0.15.1_2023-01-30/pull-4152
Normal file
19
changelog/0.15.1_2023-01-30/pull-4152
Normal file
@@ -0,0 +1,19 @@
|
||||
Enhancement: Ignore empty lock files
|
||||
|
||||
With restic 0.15.0 the checks for stale locks became much stricter than before.
|
||||
In particular, empty or unreadable locks were no longer silently ignored. This
|
||||
made restic to complain with `Load(<lock/1234567812>, 0, 0) returned error,
|
||||
retrying after 552.330144ms: load(<lock/1234567812>): invalid data returned`
|
||||
and fail in the end.
|
||||
|
||||
The error message is now clarified and the implementation changed to ignore
|
||||
empty lock files which are sometimes created as the result of a failed uploads
|
||||
on some backends.
|
||||
|
||||
Please note that unreadable lock files still have to cleaned up manually. To do
|
||||
so, you can run `restic unlock --remove-all` which removes all existing lock
|
||||
files. But first make sure that no other restic process is currently using the
|
||||
repository.
|
||||
|
||||
https://github.com/restic/restic/issues/4143
|
||||
https://github.com/restic/restic/pull/4152
|
13
changelog/0.15.1_2023-01-30/pull-4163
Normal file
13
changelog/0.15.1_2023-01-30/pull-4163
Normal file
@@ -0,0 +1,13 @@
|
||||
Bugfix: Make `self-update --output` work with new filename on Windows
|
||||
|
||||
Since restic 0.14.0 the `self-update` command did not work when a custom output
|
||||
filename was specified via the `--output` option. This has now been fixed.
|
||||
|
||||
As a workaround, either use an older restic version to run the self-update or
|
||||
create an empty file with the output filename before updating e.g. using CMD:
|
||||
|
||||
`type nul > new-file.exe`
|
||||
`restic self-update --output new-file.exe`
|
||||
|
||||
https://github.com/restic/restic/pull/4163
|
||||
https://forum.restic.net/t/self-update-windows-started-failing-after-release-of-0-15/5836
|
6
changelog/0.15.1_2023-01-30/pull-4167
Normal file
6
changelog/0.15.1_2023-01-30/pull-4167
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Add missing ETA in `backup` progress bar
|
||||
|
||||
A regression in restic 0.15.0 caused the ETA to be missing from the progress
|
||||
bar displayed by the `backup` command. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/pull/4167
|
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/textfile"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/backup"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
@@ -71,6 +72,14 @@ Exit status is 3 if some source data could not be read (incomplete snapshot crea
|
||||
term.Run(cancelCtx)
|
||||
}()
|
||||
|
||||
// use the terminal for stdout/stderr
|
||||
prevStdout, prevStderr := globalOptions.stdout, globalOptions.stderr
|
||||
defer func() {
|
||||
globalOptions.stdout, globalOptions.stderr = prevStdout, prevStderr
|
||||
}()
|
||||
stdioWrapper := ui.NewStdioWrapper(term)
|
||||
globalOptions.stdout, globalOptions.stderr = stdioWrapper.Stdout(), stdioWrapper.Stderr()
|
||||
|
||||
return runBackup(ctx, backupOptions, globalOptions, term, args)
|
||||
},
|
||||
}
|
||||
@@ -474,23 +483,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
}
|
||||
progressReporter := backup.NewProgress(progressPrinter,
|
||||
calculateProgressInterval(!gopts.Quiet, gopts.JSON))
|
||||
defer progressReporter.Done()
|
||||
|
||||
if opts.DryRun {
|
||||
repo.SetDryRun()
|
||||
}
|
||||
|
||||
// use the terminal for stdout/stderr
|
||||
prevStdout, prevStderr := gopts.stdout, gopts.stderr
|
||||
defer func() {
|
||||
gopts.stdout, gopts.stderr = prevStdout, prevStderr
|
||||
}()
|
||||
gopts.stdout, gopts.stderr = progressPrinter.Stdout(), progressPrinter.Stderr()
|
||||
|
||||
wg, wgCtx := errgroup.WithContext(ctx)
|
||||
cancelCtx, cancel := context.WithCancel(wgCtx)
|
||||
defer cancel()
|
||||
wg.Go(func() error { progressReporter.Run(cancelCtx); return nil })
|
||||
|
||||
if !gopts.JSON {
|
||||
progressPrinter.V("lock repository")
|
||||
}
|
||||
@@ -588,6 +586,10 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
targets = []string{filename}
|
||||
}
|
||||
|
||||
wg, wgCtx := errgroup.WithContext(ctx)
|
||||
cancelCtx, cancel := context.WithCancel(wgCtx)
|
||||
defer cancel()
|
||||
|
||||
if !opts.NoScan {
|
||||
sc := archiver.NewScanner(targetFS)
|
||||
sc.SelectByName = selectByNameFilter
|
||||
|
@@ -473,7 +473,7 @@ func decidePackAction(ctx context.Context, opts PruneOptions, repo restic.Reposi
|
||||
}
|
||||
|
||||
// loop over all packs and decide what to do
|
||||
bar := newProgressMax(quiet, uint64(len(indexPack)), "packs processed")
|
||||
bar := newProgressMax(!quiet, uint64(len(indexPack)), "packs processed")
|
||||
err := repo.List(ctx, restic.PackFile, func(id restic.ID, packSize int64) error {
|
||||
p, ok := indexPack[id]
|
||||
if !ok {
|
||||
|
@@ -42,7 +42,7 @@ import (
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var version = "0.15.0"
|
||||
var version = "0.15.1"
|
||||
|
||||
// TimeFormat is the format used for all timestamps printed by restic.
|
||||
const TimeFormat = "2006-01-02 15:04:05"
|
||||
|
@@ -1623,10 +1623,7 @@ func testPruneVariants(t *testing.T, unsafeNoSpaceRecovery bool) {
|
||||
})
|
||||
}
|
||||
|
||||
func testPrune(t *testing.T, pruneOpts PruneOptions, checkOpts CheckOptions) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
func createPrunableRepo(t *testing.T, env *testEnvironment) {
|
||||
testSetupBackupData(t, env)
|
||||
opts := BackupOptions{}
|
||||
|
||||
@@ -1644,6 +1641,13 @@ func testPrune(t *testing.T, pruneOpts PruneOptions, checkOpts CheckOptions) {
|
||||
|
||||
testRunForgetJSON(t, env.gopts)
|
||||
testRunForget(t, env.gopts, firstSnapshot[0].String())
|
||||
}
|
||||
|
||||
func testPrune(t *testing.T, pruneOpts PruneOptions, checkOpts CheckOptions) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
createPrunableRepo(t, env)
|
||||
testRunPrune(t, env.gopts, pruneOpts)
|
||||
rtest.OK(t, runCheck(context.TODO(), checkOpts, env.gopts, nil))
|
||||
}
|
||||
@@ -1826,27 +1830,10 @@ func TestListOnce(t *testing.T) {
|
||||
env.gopts.backendTestHook = func(r restic.Backend) (restic.Backend, error) {
|
||||
return newListOnceBackend(r), nil
|
||||
}
|
||||
|
||||
pruneOpts := PruneOptions{MaxUnused: "0"}
|
||||
checkOpts := CheckOptions{ReadData: true, CheckUnused: true}
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
opts := BackupOptions{}
|
||||
|
||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
||||
firstSnapshot := testRunList(t, "snapshots", env.gopts)
|
||||
rtest.Assert(t, len(firstSnapshot) == 1,
|
||||
"expected one snapshot, got %v", firstSnapshot)
|
||||
|
||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts)
|
||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts)
|
||||
|
||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
||||
rtest.Assert(t, len(snapshotIDs) == 3,
|
||||
"expected 3 snapshot, got %v", snapshotIDs)
|
||||
|
||||
testRunForgetJSON(t, env.gopts)
|
||||
testRunForget(t, env.gopts, firstSnapshot[0].String())
|
||||
createPrunableRepo(t, env)
|
||||
testRunPrune(t, env.gopts, pruneOpts)
|
||||
rtest.OK(t, runCheck(context.TODO(), checkOpts, env.gopts, nil))
|
||||
|
||||
|
@@ -2,11 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
@@ -44,8 +44,11 @@ func lockRepository(ctx context.Context, repo restic.Repository, exclusive bool)
|
||||
}
|
||||
|
||||
lock, err := lockFn(ctx, repo)
|
||||
if restic.IsInvalidLock(err) {
|
||||
return nil, ctx, errors.Fatalf("%v\n\nthe `unlock --remove-all` command can be used to remove invalid locks. Make sure that no other restic process is accessing the repository when running the command", err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ctx, fmt.Errorf("unable to create lock in backend: %w", err)
|
||||
return nil, ctx, errors.Fatalf("unable to create lock in backend: %v", err)
|
||||
}
|
||||
debug.Log("create lock %p (exclusive %v)", lock, exclusive)
|
||||
|
||||
|
@@ -37,7 +37,7 @@ func newProgressMax(show bool, max uint64, description string) *progress.Counter
|
||||
interval := calculateProgressInterval(show, false)
|
||||
canUpdateStatus := stdoutCanUpdateStatus()
|
||||
|
||||
return progress.New(interval, max, func(v uint64, max uint64, d time.Duration, final bool) {
|
||||
return progress.NewCounter(interval, max, func(v uint64, max uint64, d time.Duration, final bool) {
|
||||
var status string
|
||||
if max == 0 {
|
||||
status = fmt.Sprintf("[%s] %d %s",
|
||||
|
10
go.mod
10
go.mod
@@ -1,7 +1,7 @@
|
||||
module github.com/restic/restic
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.28.1
|
||||
cloud.google.com/go/storage v1.29.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1
|
||||
github.com/anacrolix/fuse v0.2.0
|
||||
@@ -12,9 +12,9 @@ require (
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.1
|
||||
github.com/juju/ratelimit v1.0.2
|
||||
github.com/klauspost/compress v1.15.14
|
||||
github.com/kurin/blazer v0.5.4-0.20211030221322-ba894c124ac6
|
||||
github.com/minio/minio-go/v7 v7.0.46
|
||||
github.com/klauspost/compress v1.15.15
|
||||
github.com/kurin/blazer v0.5.4-0.20230113224640-3887e1ec64b5
|
||||
github.com/minio/minio-go/v7 v7.0.47
|
||||
github.com/minio/sha256-simd v1.0.0
|
||||
github.com/ncw/swift/v2 v2.0.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -31,7 +31,7 @@ require (
|
||||
golang.org/x/sys v0.4.0
|
||||
golang.org/x/term v0.4.0
|
||||
golang.org/x/text v0.6.0
|
||||
google.golang.org/api v0.106.0
|
||||
google.golang.org/api v0.108.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
20
go.sum
20
go.sum
@@ -8,8 +8,8 @@ cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2Aawl
|
||||
cloud.google.com/go/iam v0.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI=
|
||||
cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
|
||||
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
|
||||
cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
|
||||
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 h1:VuHAcMq8pU1IWNT/m5yRaGqbK0BiQKHT8X4DTp9CHdI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0/go.mod h1:tZoQYdDZNOiIjdSn0dVWVfl0NEPGOJqVLzSrcFk4Is0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
|
||||
@@ -103,21 +103,21 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc=
|
||||
github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
||||
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
||||
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kurin/blazer v0.5.4-0.20211030221322-ba894c124ac6 h1:nz7i1au+nDzgExfqW5Zl6q85XNTvYoGnM5DHiQC0yYs=
|
||||
github.com/kurin/blazer v0.5.4-0.20211030221322-ba894c124ac6/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU=
|
||||
github.com/kurin/blazer v0.5.4-0.20230113224640-3887e1ec64b5 h1:OUlGa6AAolmjyPtILbMJ8vHayz5wd4wBUloheGcMhfA=
|
||||
github.com/kurin/blazer v0.5.4-0.20230113224640-3887e1ec64b5/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.46 h1:Vo3tNmNXuj7ME5qrvN4iadO7b4mzu/RSFdUkUhaPldk=
|
||||
github.com/minio/minio-go/v7 v7.0.46/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
|
||||
github.com/minio/minio-go/v7 v7.0.47 h1:sLiuCKGSIcn/MI6lREmTzX91DX/oRau4ia0j6e6eOSs=
|
||||
github.com/minio/minio-go/v7 v7.0.47/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -237,8 +237,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.106.0 h1:ffmW0faWCwKkpbbtvlY/K/8fUl+JKvNS5CVzRoyfCv8=
|
||||
google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
||||
google.golang.org/api v0.108.0 h1:WVBc/faN0DkKtR43Q/7+tPny9ZoLZdIiAyG5Q9vFClg=
|
||||
google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
|
14
internal/cache/file.go
vendored
14
internal/cache/file.go
vendored
@@ -7,6 +7,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/restic/restic/internal/backend"
|
||||
"github.com/restic/restic/internal/crypto"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
@@ -30,11 +31,6 @@ func (c *Cache) canBeCached(t restic.FileType) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
type readCloser struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Load returns a reader that yields the contents of the file with the
|
||||
// given handle. rd must be closed after use. If an error is returned, the
|
||||
// ReadCloser is nil.
|
||||
@@ -75,12 +71,10 @@ func (c *Cache) load(h restic.Handle, length int, offset int64) (io.ReadCloser,
|
||||
}
|
||||
}
|
||||
|
||||
rd := readCloser{Reader: f, Closer: f}
|
||||
if length > 0 {
|
||||
rd.Reader = io.LimitReader(f, int64(length))
|
||||
if length <= 0 {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
return rd, nil
|
||||
return backend.LimitReadCloser(f, int64(length)), nil
|
||||
}
|
||||
|
||||
// Save saves a file in the cache.
|
||||
|
@@ -317,9 +317,9 @@ type blobJSON struct {
|
||||
}
|
||||
|
||||
// generatePackList returns a list of packs.
|
||||
func (idx *Index) generatePackList() ([]*packJSON, error) {
|
||||
list := []*packJSON{}
|
||||
packs := make(map[restic.ID]*packJSON)
|
||||
func (idx *Index) generatePackList() ([]packJSON, error) {
|
||||
list := make([]packJSON, 0, len(idx.packs))
|
||||
packs := make(map[restic.ID]int, len(list)) // Maps to index in list.
|
||||
|
||||
for typ := range idx.byType {
|
||||
m := &idx.byType[typ]
|
||||
@@ -329,18 +329,13 @@ func (idx *Index) generatePackList() ([]*packJSON, error) {
|
||||
panic("null pack id")
|
||||
}
|
||||
|
||||
debug.Log("handle blob %v", e.id)
|
||||
|
||||
// see if pack is already in map
|
||||
p, ok := packs[packID]
|
||||
i, ok := packs[packID]
|
||||
if !ok {
|
||||
// else create new pack
|
||||
p = &packJSON{ID: packID}
|
||||
|
||||
// and append it to the list and map
|
||||
list = append(list, p)
|
||||
packs[p.ID] = p
|
||||
i = len(list)
|
||||
list = append(list, packJSON{ID: packID})
|
||||
packs[packID] = i
|
||||
}
|
||||
p := &list[i]
|
||||
|
||||
// add blob
|
||||
p.Blobs = append(p.Blobs, blobJSON{
|
||||
@@ -355,14 +350,12 @@ func (idx *Index) generatePackList() ([]*packJSON, error) {
|
||||
})
|
||||
}
|
||||
|
||||
debug.Log("done")
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
type jsonIndex struct {
|
||||
Supersedes restic.IDs `json:"supersedes,omitempty"`
|
||||
Packs []*packJSON `json:"packs"`
|
||||
Supersedes restic.IDs `json:"supersedes,omitempty"`
|
||||
Packs []packJSON `json:"packs"`
|
||||
}
|
||||
|
||||
// Encode writes the JSON serialization of the index to the writer w.
|
||||
|
@@ -3,6 +3,7 @@ package index_test
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -405,6 +406,26 @@ func BenchmarkDecodeIndexParallel(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIndex(b *testing.B) {
|
||||
for _, n := range []int{100, 1000, 10000} {
|
||||
idx, _ := createRandomIndex(rand.New(rand.NewSource(0)), n)
|
||||
|
||||
b.Run(fmt.Sprint(n), func(b *testing.B) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := idx.Encode(buf)
|
||||
rtest.OK(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
_ = idx.Encode(buf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexUnserializeOld(t *testing.T) {
|
||||
idx, oldFormat, err := index.DecodeIndex(docOldExample, restic.NewRandomID())
|
||||
rtest.OK(t, err)
|
||||
|
@@ -188,6 +188,7 @@ func (r *Repository) LoadUnpacked(ctx context.Context, t restic.FileType, id res
|
||||
|
||||
h := restic.Handle{Type: t, Name: id.String()}
|
||||
retriedInvalidData := false
|
||||
var dataErr error
|
||||
err := r.be.Load(ctx, h, 0, 0, func(rd io.Reader) error {
|
||||
// make sure this call is idempotent, in case an error occurs
|
||||
wr := bytes.NewBuffer(buf[:0])
|
||||
@@ -202,13 +203,20 @@ func (r *Repository) LoadUnpacked(ctx context.Context, t restic.FileType, id res
|
||||
if !retriedInvalidData {
|
||||
retriedInvalidData = true
|
||||
} else {
|
||||
// with a canceled context there is not guarantee which error will
|
||||
// be returned by `be.Load`.
|
||||
dataErr = fmt.Errorf("load(%v): %w", h, restic.ErrInvalidData)
|
||||
cancel()
|
||||
}
|
||||
return errors.Errorf("load(%v): invalid data returned", h)
|
||||
return restic.ErrInvalidData
|
||||
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if dataErr != nil {
|
||||
return nil, dataErr
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/restic/restic/internal/backend/local"
|
||||
"github.com/restic/restic/internal/backend/mem"
|
||||
"github.com/restic/restic/internal/backend/retry"
|
||||
"github.com/restic/restic/internal/crypto"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/test"
|
||||
@@ -97,11 +98,14 @@ func TestRepositoryWithVersion(t testing.TB, version uint) restic.Repository {
|
||||
|
||||
// TestOpenLocal opens a local repository.
|
||||
func TestOpenLocal(t testing.TB, dir string) (r restic.Repository) {
|
||||
var be restic.Backend
|
||||
be, err := local.Open(context.TODO(), local.Config{Path: dir, Connections: 2})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
be = retry.New(be, 3, nil, nil)
|
||||
|
||||
repo, err := New(be, Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@@ -93,7 +93,7 @@ func TestFindUsedBlobs(t *testing.T) {
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
|
||||
p := progress.New(time.Second, findTestSnapshots, func(value uint64, total uint64, runtime time.Duration, final bool) {})
|
||||
p := progress.NewCounter(time.Second, findTestSnapshots, func(value uint64, total uint64, runtime time.Duration, final bool) {})
|
||||
defer p.Done()
|
||||
|
||||
for i, sn := range snapshots {
|
||||
@@ -142,7 +142,7 @@ func TestMultiFindUsedBlobs(t *testing.T) {
|
||||
want.Merge(loadIDSet(t, goldenFilename))
|
||||
}
|
||||
|
||||
p := progress.New(time.Second, findTestSnapshots, func(value uint64, total uint64, runtime time.Duration, final bool) {})
|
||||
p := progress.NewCounter(time.Second, findTestSnapshots, func(value uint64, total uint64, runtime time.Duration, final bool) {})
|
||||
defer p.Done()
|
||||
|
||||
// run twice to check progress bar handling of duplicate tree roots
|
||||
|
@@ -60,6 +60,27 @@ func IsAlreadyLocked(err error) bool {
|
||||
return errors.As(err, &e)
|
||||
}
|
||||
|
||||
// invalidLockError is returned when NewLock or NewExclusiveLock fail due
|
||||
// to an invalid lock.
|
||||
type invalidLockError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *invalidLockError) Error() string {
|
||||
return fmt.Sprintf("invalid lock file: %v", e.err)
|
||||
}
|
||||
|
||||
func (e *invalidLockError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// IsInvalidLock returns true iff err indicates that locking failed due to
|
||||
// an invalid lock.
|
||||
func IsInvalidLock(err error) bool {
|
||||
var e *invalidLockError
|
||||
return errors.As(err, &e)
|
||||
}
|
||||
|
||||
// NewLock returns a new, non-exclusive lock for the repository. If an
|
||||
// exclusive lock is already held by another process, it returns an error
|
||||
// that satisfies IsAlreadyLocked.
|
||||
@@ -146,7 +167,7 @@ func (l *Lock) checkForOtherLocks(ctx context.Context) error {
|
||||
// if we cannot load a lock then it is unclear whether it can be ignored
|
||||
// it could either be invalid or just unreadable due to network/permission problems
|
||||
debug.Log("ignore lock %v: %v", id, err)
|
||||
return errors.Fatal(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if l.Exclusive {
|
||||
@@ -168,6 +189,9 @@ func (l *Lock) checkForOtherLocks(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if errors.Is(err, ErrInvalidData) {
|
||||
return &invalidLockError{err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -198,7 +222,7 @@ var StaleLockTimeout = 30 * time.Minute
|
||||
func (l *Lock) Stale() bool {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
debug.Log("testing if lock %v for process %d is stale", l, l.PID)
|
||||
debug.Log("testing if lock %v for process %d is stale", l.lockID, l.PID)
|
||||
if time.Since(l.Time) > StaleLockTimeout {
|
||||
debug.Log("lock is stale, timestamp is too old: %v\n", l.Time)
|
||||
return true
|
||||
@@ -336,6 +360,12 @@ func ForAllLocks(ctx context.Context, repo Repository, excludeID *ID, fn func(ID
|
||||
if excludeID != nil && id.Equal(*excludeID) {
|
||||
return nil
|
||||
}
|
||||
if size == 0 {
|
||||
// Ignore empty lock files as some backends do not guarantee atomic uploads.
|
||||
// These may leave empty files behind if an upload was interrupted between
|
||||
// creating the file and writing its data.
|
||||
return nil
|
||||
}
|
||||
lock, err := LoadLock(ctx, repo, id)
|
||||
|
||||
m.Lock()
|
||||
|
@@ -4,10 +4,14 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/restic/restic/internal/crypto"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// ErrInvalidData is used to report that a file is corrupted
|
||||
var ErrInvalidData = errors.New("invalid data returned")
|
||||
|
||||
// Repository stores data in a backend. It provides high-level functions and
|
||||
// transparently encrypts/decrypts data.
|
||||
type Repository interface {
|
||||
|
44
internal/selfupdate/download_test.go
Normal file
44
internal/selfupdate/download_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package selfupdate
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func TestExtractToFileZip(t *testing.T) {
|
||||
printf := func(string, ...interface{}) {}
|
||||
dir := t.TempDir()
|
||||
|
||||
ext := "zip"
|
||||
data := []byte("Hello World!")
|
||||
|
||||
// create dummy archive
|
||||
var archive bytes.Buffer
|
||||
zw := zip.NewWriter(&archive)
|
||||
w, err := zw.CreateHeader(&zip.FileHeader{
|
||||
Name: "example.exe",
|
||||
UncompressedSize64: uint64(len(data)),
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
_, err = w.Write(data[:])
|
||||
rtest.OK(t, err)
|
||||
rtest.OK(t, zw.Close())
|
||||
|
||||
// run twice to test creating a new file and overwriting
|
||||
for i := 0; i < 2; i++ {
|
||||
outfn := filepath.Join(dir, ext+"-out")
|
||||
rtest.OK(t, extractToFile(archive.Bytes(), "src."+ext, outfn, printf))
|
||||
|
||||
outdata, err := os.ReadFile(outfn)
|
||||
rtest.OK(t, err)
|
||||
rtest.Assert(t, bytes.Equal(data[:], outdata), "%v contains wrong data", outfn)
|
||||
|
||||
// overwrite to test the file is properly overwritten
|
||||
rtest.OK(t, os.WriteFile(outfn, []byte{1, 2, 3}, 0))
|
||||
}
|
||||
}
|
@@ -7,11 +7,18 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
)
|
||||
|
||||
// Rename (rather than remove) the running version. The running binary will be locked
|
||||
// on Windows and cannot be removed while still executing.
|
||||
func removeResticBinary(dir, target string) error {
|
||||
// nothing to do if the target does not exist
|
||||
if _, err := os.Stat(target); errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
backup := filepath.Join(dir, filepath.Base(target)+".bak")
|
||||
if _, err := os.Stat(backup); err == nil {
|
||||
_ = os.Remove(backup)
|
||||
|
@@ -15,7 +15,6 @@ import (
|
||||
// JSONProgress reports progress for the `backup` command in JSON.
|
||||
type JSONProgress struct {
|
||||
*ui.Message
|
||||
*ui.StdioWrapper
|
||||
|
||||
term *termstatus.Terminal
|
||||
v uint
|
||||
@@ -27,10 +26,9 @@ var _ ProgressPrinter = &JSONProgress{}
|
||||
// NewJSONProgress returns a new backup progress reporter.
|
||||
func NewJSONProgress(term *termstatus.Terminal, verbosity uint) *JSONProgress {
|
||||
return &JSONProgress{
|
||||
Message: ui.NewMessage(term, verbosity),
|
||||
StdioWrapper: ui.NewStdioWrapper(term),
|
||||
term: term,
|
||||
v: verbosity,
|
||||
Message: ui.NewMessage(term, verbosity),
|
||||
term: term,
|
||||
v: verbosity,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,14 +1,12 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/archiver"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui/signals"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
// A ProgressPrinter can print various progress messages.
|
||||
@@ -22,10 +20,6 @@ type ProgressPrinter interface {
|
||||
Finish(snapshotID restic.ID, start time.Time, summary *Summary, dryRun bool)
|
||||
Reset()
|
||||
|
||||
// ui.StdioWrapper
|
||||
Stdout() io.WriteCloser
|
||||
Stderr() io.WriteCloser
|
||||
|
||||
P(msg string, args ...interface{})
|
||||
V(msg string, args ...interface{})
|
||||
}
|
||||
@@ -46,10 +40,10 @@ type Summary struct {
|
||||
|
||||
// Progress reports progress for the `backup` command.
|
||||
type Progress struct {
|
||||
progress.Updater
|
||||
mu sync.Mutex
|
||||
|
||||
interval time.Duration
|
||||
start time.Time
|
||||
start time.Time
|
||||
|
||||
scanStarted, scanFinished bool
|
||||
|
||||
@@ -57,66 +51,37 @@ type Progress struct {
|
||||
processed, total Counter
|
||||
errors uint
|
||||
|
||||
closed chan struct{}
|
||||
|
||||
summary Summary
|
||||
printer ProgressPrinter
|
||||
}
|
||||
|
||||
func NewProgress(printer ProgressPrinter, interval time.Duration) *Progress {
|
||||
return &Progress{
|
||||
interval: interval,
|
||||
start: time.Now(),
|
||||
|
||||
p := &Progress{
|
||||
start: time.Now(),
|
||||
currentFiles: make(map[string]struct{}),
|
||||
closed: make(chan struct{}),
|
||||
|
||||
printer: printer,
|
||||
printer: printer,
|
||||
}
|
||||
}
|
||||
p.Updater = *progress.NewUpdater(interval, func(runtime time.Duration, final bool) {
|
||||
if final {
|
||||
p.printer.Reset()
|
||||
} else {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if !p.scanStarted {
|
||||
return
|
||||
}
|
||||
|
||||
// Run regularly updates the status lines. It should be called in a separate
|
||||
// goroutine.
|
||||
func (p *Progress) Run(ctx context.Context) {
|
||||
defer close(p.closed)
|
||||
// Reset status when finished
|
||||
defer p.printer.Reset()
|
||||
var secondsRemaining uint64
|
||||
if p.scanFinished {
|
||||
secs := float64(runtime / time.Second)
|
||||
todo := float64(p.total.Bytes - p.processed.Bytes)
|
||||
secondsRemaining = uint64(secs / float64(p.processed.Bytes) * todo)
|
||||
}
|
||||
|
||||
var tick <-chan time.Time
|
||||
if p.interval != 0 {
|
||||
t := time.NewTicker(p.interval)
|
||||
defer t.Stop()
|
||||
tick = t.C
|
||||
}
|
||||
|
||||
signalsCh := signals.GetProgressChannel()
|
||||
|
||||
for {
|
||||
var now time.Time
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case now = <-tick:
|
||||
case <-signalsCh:
|
||||
now = time.Now()
|
||||
p.printer.Update(p.total, p.processed, p.errors, p.currentFiles, p.start, secondsRemaining)
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
if !p.scanStarted {
|
||||
p.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
var secondsRemaining uint64
|
||||
if p.scanFinished {
|
||||
secs := float64(now.Sub(p.start) / time.Second)
|
||||
todo := float64(p.total.Bytes - p.processed.Bytes)
|
||||
secondsRemaining = uint64(secs / float64(p.processed.Bytes) * todo)
|
||||
}
|
||||
|
||||
p.printer.Update(p.total, p.processed, p.errors, p.currentFiles, p.start, secondsRemaining)
|
||||
p.mu.Unlock()
|
||||
}
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
// Error is the error callback function for the archiver, it prints the error and returns nil.
|
||||
@@ -234,6 +199,7 @@ func (p *Progress) ReportTotal(item string, s archiver.ScanStats) {
|
||||
p.scanStarted = true
|
||||
|
||||
if item == "" {
|
||||
p.scanFinished = true
|
||||
p.printer.ReportTotal(item, p.start, s)
|
||||
}
|
||||
}
|
||||
@@ -241,6 +207,6 @@ func (p *Progress) ReportTotal(item string, s archiver.ScanStats) {
|
||||
// Finish prints the finishing messages.
|
||||
func (p *Progress) Finish(snapshotID restic.ID, dryrun bool) {
|
||||
// wait for the status update goroutine to shut down
|
||||
<-p.closed
|
||||
p.Updater.Done()
|
||||
p.printer.Finish(snapshotID, p.start, &p.summary, dryrun)
|
||||
}
|
||||
|
@@ -1,8 +1,6 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -45,9 +43,6 @@ func (p *mockPrinter) Finish(id restic.ID, _ time.Time, summary *Summary, dryRun
|
||||
|
||||
func (p *mockPrinter) Reset() {}
|
||||
|
||||
func (p *mockPrinter) Stdout() io.WriteCloser { return nil }
|
||||
func (p *mockPrinter) Stderr() io.WriteCloser { return nil }
|
||||
|
||||
func (p *mockPrinter) P(msg string, args ...interface{}) {}
|
||||
func (p *mockPrinter) V(msg string, args ...interface{}) {}
|
||||
|
||||
@@ -57,9 +52,6 @@ func TestProgress(t *testing.T) {
|
||||
prnt := &mockPrinter{}
|
||||
prog := NewProgress(prnt, time.Millisecond)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go prog.Run(ctx)
|
||||
|
||||
prog.StartFile("foo")
|
||||
prog.CompleteBlob(1024)
|
||||
|
||||
@@ -71,7 +63,6 @@ func TestProgress(t *testing.T) {
|
||||
prog.CompleteItem("foo", nil, &node, archiver.ItemStats{}, 0)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
id := restic.NewRandomID()
|
||||
prog.Finish(id, false)
|
||||
|
||||
|
@@ -14,7 +14,6 @@ import (
|
||||
// TextProgress reports progress for the `backup` command.
|
||||
type TextProgress struct {
|
||||
*ui.Message
|
||||
*ui.StdioWrapper
|
||||
|
||||
term *termstatus.Terminal
|
||||
}
|
||||
@@ -25,9 +24,8 @@ var _ ProgressPrinter = &TextProgress{}
|
||||
// NewTextProgress returns a new backup progress reporter.
|
||||
func NewTextProgress(term *termstatus.Terminal, verbosity uint) *TextProgress {
|
||||
return &TextProgress{
|
||||
Message: ui.NewMessage(term, verbosity),
|
||||
StdioWrapper: ui.NewStdioWrapper(term),
|
||||
term: term,
|
||||
Message: ui.NewMessage(term, verbosity),
|
||||
term: term,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,9 +3,6 @@ package progress
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/ui/signals"
|
||||
)
|
||||
|
||||
// A Func is a callback for a Counter.
|
||||
@@ -19,32 +16,22 @@ type Func func(value uint64, total uint64, runtime time.Duration, final bool)
|
||||
//
|
||||
// The Func is also called when SIGUSR1 (or SIGINFO, on BSD) is received.
|
||||
type Counter struct {
|
||||
report Func
|
||||
start time.Time
|
||||
stopped chan struct{} // Closed by run.
|
||||
stop chan struct{} // Close to stop run.
|
||||
tick *time.Ticker
|
||||
Updater
|
||||
|
||||
valueMutex sync.Mutex
|
||||
value uint64
|
||||
max uint64
|
||||
}
|
||||
|
||||
// New starts a new Counter.
|
||||
func New(interval time.Duration, total uint64, report Func) *Counter {
|
||||
// NewCounter starts a new Counter.
|
||||
func NewCounter(interval time.Duration, total uint64, report Func) *Counter {
|
||||
c := &Counter{
|
||||
report: report,
|
||||
start: time.Now(),
|
||||
stopped: make(chan struct{}),
|
||||
stop: make(chan struct{}),
|
||||
max: total,
|
||||
max: total,
|
||||
}
|
||||
|
||||
if interval > 0 {
|
||||
c.tick = time.NewTicker(interval)
|
||||
}
|
||||
|
||||
go c.run()
|
||||
c.Updater = *NewUpdater(interval, func(runtime time.Duration, final bool) {
|
||||
v, max := c.Get()
|
||||
report(v, max, runtime, final)
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -69,18 +56,6 @@ func (c *Counter) SetMax(max uint64) {
|
||||
c.valueMutex.Unlock()
|
||||
}
|
||||
|
||||
// Done tells a Counter to stop and waits for it to report its final value.
|
||||
func (c *Counter) Done() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
if c.tick != nil {
|
||||
c.tick.Stop()
|
||||
}
|
||||
close(c.stop)
|
||||
<-c.stopped // Wait for last progress report.
|
||||
}
|
||||
|
||||
// Get returns the current value and the maximum of c.
|
||||
// This method is concurrency-safe.
|
||||
func (c *Counter) Get() (v, max uint64) {
|
||||
@@ -91,32 +66,8 @@ func (c *Counter) Get() (v, max uint64) {
|
||||
return v, max
|
||||
}
|
||||
|
||||
func (c *Counter) run() {
|
||||
defer close(c.stopped)
|
||||
defer func() {
|
||||
// Must be a func so that time.Since isn't called at defer time.
|
||||
v, max := c.Get()
|
||||
c.report(v, max, time.Since(c.start), true)
|
||||
}()
|
||||
|
||||
var tick <-chan time.Time
|
||||
if c.tick != nil {
|
||||
tick = c.tick.C
|
||||
}
|
||||
signalsCh := signals.GetProgressChannel()
|
||||
for {
|
||||
var now time.Time
|
||||
|
||||
select {
|
||||
case now = <-tick:
|
||||
case sig := <-signalsCh:
|
||||
debug.Log("Signal received: %v\n", sig)
|
||||
now = time.Now()
|
||||
case <-c.stop:
|
||||
return
|
||||
}
|
||||
|
||||
v, max := c.Get()
|
||||
c.report(v, max, now.Sub(c.start), false)
|
||||
func (c *Counter) Done() {
|
||||
if c != nil {
|
||||
c.Updater.Done()
|
||||
}
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ func TestCounter(t *testing.T) {
|
||||
lastTotal = total
|
||||
ncalls++
|
||||
}
|
||||
c := progress.New(10*time.Millisecond, startTotal, report)
|
||||
c := progress.NewCounter(10*time.Millisecond, startTotal, report)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
@@ -63,24 +63,6 @@ func TestCounterNil(t *testing.T) {
|
||||
// Shouldn't panic.
|
||||
var c *progress.Counter
|
||||
c.Add(1)
|
||||
c.SetMax(42)
|
||||
c.Done()
|
||||
}
|
||||
|
||||
func TestCounterNoTick(t *testing.T) {
|
||||
finalSeen := false
|
||||
otherSeen := false
|
||||
|
||||
report := func(value, total uint64, d time.Duration, final bool) {
|
||||
if final {
|
||||
finalSeen = true
|
||||
} else {
|
||||
otherSeen = true
|
||||
}
|
||||
}
|
||||
c := progress.New(0, 1, report)
|
||||
time.Sleep(time.Millisecond)
|
||||
c.Done()
|
||||
|
||||
test.Assert(t, finalSeen, "final call did not happen")
|
||||
test.Assert(t, !otherSeen, "unexpected status update")
|
||||
}
|
||||
|
84
internal/ui/progress/updater.go
Normal file
84
internal/ui/progress/updater.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package progress
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/ui/signals"
|
||||
)
|
||||
|
||||
// An UpdateFunc is a callback for a (progress) Updater.
|
||||
//
|
||||
// The final argument is true if Updater.Done has been called,
|
||||
// which means that the current call will be the last.
|
||||
type UpdateFunc func(runtime time.Duration, final bool)
|
||||
|
||||
// An Updater controls a goroutine that periodically calls an UpdateFunc.
|
||||
//
|
||||
// The UpdateFunc is also called when SIGUSR1 (or SIGINFO, on BSD) is received.
|
||||
type Updater struct {
|
||||
report UpdateFunc
|
||||
start time.Time
|
||||
stopped chan struct{} // Closed by run.
|
||||
stop chan struct{} // Close to stop run.
|
||||
tick *time.Ticker
|
||||
}
|
||||
|
||||
// NewUpdater starts a new Updater.
|
||||
func NewUpdater(interval time.Duration, report UpdateFunc) *Updater {
|
||||
c := &Updater{
|
||||
report: report,
|
||||
start: time.Now(),
|
||||
stopped: make(chan struct{}),
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
if interval > 0 {
|
||||
c.tick = time.NewTicker(interval)
|
||||
}
|
||||
|
||||
go c.run()
|
||||
return c
|
||||
}
|
||||
|
||||
// Done tells an Updater to stop and waits for it to report its final value.
|
||||
// Later calls do nothing.
|
||||
func (c *Updater) Done() {
|
||||
if c == nil || c.stop == nil {
|
||||
return
|
||||
}
|
||||
if c.tick != nil {
|
||||
c.tick.Stop()
|
||||
}
|
||||
close(c.stop)
|
||||
<-c.stopped // Wait for last progress report.
|
||||
c.stop = nil
|
||||
}
|
||||
|
||||
func (c *Updater) run() {
|
||||
defer close(c.stopped)
|
||||
defer func() {
|
||||
// Must be a func so that time.Since isn't called at defer time.
|
||||
c.report(time.Since(c.start), true)
|
||||
}()
|
||||
|
||||
var tick <-chan time.Time
|
||||
if c.tick != nil {
|
||||
tick = c.tick.C
|
||||
}
|
||||
signalsCh := signals.GetProgressChannel()
|
||||
for {
|
||||
var now time.Time
|
||||
|
||||
select {
|
||||
case now = <-tick:
|
||||
case sig := <-signalsCh:
|
||||
debug.Log("Signal received: %v\n", sig)
|
||||
now = time.Now()
|
||||
case <-c.stop:
|
||||
return
|
||||
}
|
||||
|
||||
c.report(now.Sub(c.start), false)
|
||||
}
|
||||
}
|
52
internal/ui/progress/updater_test.go
Normal file
52
internal/ui/progress/updater_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package progress_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func TestUpdater(t *testing.T) {
|
||||
finalSeen := false
|
||||
var ncalls int
|
||||
|
||||
report := func(d time.Duration, final bool) {
|
||||
if final {
|
||||
finalSeen = true
|
||||
}
|
||||
ncalls++
|
||||
}
|
||||
c := progress.NewUpdater(10*time.Millisecond, report)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
c.Done()
|
||||
|
||||
test.Assert(t, finalSeen, "final call did not happen")
|
||||
test.Assert(t, ncalls > 0, "no progress was reported")
|
||||
}
|
||||
|
||||
func TestUpdaterStopTwice(t *testing.T) {
|
||||
c := progress.NewUpdater(0, func(runtime time.Duration, final bool) {})
|
||||
c.Done()
|
||||
c.Done()
|
||||
}
|
||||
|
||||
func TestUpdaterNoTick(t *testing.T) {
|
||||
finalSeen := false
|
||||
otherSeen := false
|
||||
|
||||
report := func(d time.Duration, final bool) {
|
||||
if final {
|
||||
finalSeen = true
|
||||
} else {
|
||||
otherSeen = true
|
||||
}
|
||||
}
|
||||
c := progress.NewUpdater(0, report)
|
||||
time.Sleep(time.Millisecond)
|
||||
c.Done()
|
||||
|
||||
test.Assert(t, finalSeen, "final call did not happen")
|
||||
test.Assert(t, !otherSeen, "unexpected status update")
|
||||
}
|
Reference in New Issue
Block a user