mirror of
https://github.com/restic/restic.git
synced 2025-08-26 07:49:25 +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"
|
- package-ecosystem: "gomod"
|
||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "weekly"
|
||||||
|
|
||||||
# Dependencies listed in .github/workflows/*.yml
|
# Dependencies listed in .github/workflows/*.yml
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
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)
|
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/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/textfile"
|
"github.com/restic/restic/internal/textfile"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/backup"
|
"github.com/restic/restic/internal/ui/backup"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"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)
|
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)
|
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,
|
progressReporter := backup.NewProgress(progressPrinter,
|
||||||
calculateProgressInterval(!gopts.Quiet, gopts.JSON))
|
calculateProgressInterval(!gopts.Quiet, gopts.JSON))
|
||||||
|
defer progressReporter.Done()
|
||||||
|
|
||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
repo.SetDryRun()
|
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 {
|
if !gopts.JSON {
|
||||||
progressPrinter.V("lock repository")
|
progressPrinter.V("lock repository")
|
||||||
}
|
}
|
||||||
@@ -588,6 +586,10 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
targets = []string{filename}
|
targets = []string{filename}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wg, wgCtx := errgroup.WithContext(ctx)
|
||||||
|
cancelCtx, cancel := context.WithCancel(wgCtx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
if !opts.NoScan {
|
if !opts.NoScan {
|
||||||
sc := archiver.NewScanner(targetFS)
|
sc := archiver.NewScanner(targetFS)
|
||||||
sc.SelectByName = selectByNameFilter
|
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
|
// 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 {
|
err := repo.List(ctx, restic.PackFile, func(id restic.ID, packSize int64) error {
|
||||||
p, ok := indexPack[id]
|
p, ok := indexPack[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@@ -42,7 +42,7 @@ import (
|
|||||||
"golang.org/x/term"
|
"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.
|
// TimeFormat is the format used for all timestamps printed by restic.
|
||||||
const TimeFormat = "2006-01-02 15:04:05"
|
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) {
|
func createPrunableRepo(t *testing.T, env *testEnvironment) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
@@ -1644,6 +1641,13 @@ func testPrune(t *testing.T, pruneOpts PruneOptions, checkOpts CheckOptions) {
|
|||||||
|
|
||||||
testRunForgetJSON(t, env.gopts)
|
testRunForgetJSON(t, env.gopts)
|
||||||
testRunForget(t, env.gopts, firstSnapshot[0].String())
|
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)
|
testRunPrune(t, env.gopts, pruneOpts)
|
||||||
rtest.OK(t, runCheck(context.TODO(), checkOpts, env.gopts, nil))
|
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) {
|
env.gopts.backendTestHook = func(r restic.Backend) (restic.Backend, error) {
|
||||||
return newListOnceBackend(r), nil
|
return newListOnceBackend(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pruneOpts := PruneOptions{MaxUnused: "0"}
|
pruneOpts := PruneOptions{MaxUnused: "0"}
|
||||||
checkOpts := CheckOptions{ReadData: true, CheckUnused: true}
|
checkOpts := CheckOptions{ReadData: true, CheckUnused: true}
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
createPrunableRepo(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())
|
|
||||||
testRunPrune(t, env.gopts, pruneOpts)
|
testRunPrune(t, env.gopts, pruneOpts)
|
||||||
rtest.OK(t, runCheck(context.TODO(), checkOpts, env.gopts, nil))
|
rtest.OK(t, runCheck(context.TODO(), checkOpts, env.gopts, nil))
|
||||||
|
|
||||||
|
@@ -2,11 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/restic"
|
"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)
|
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 {
|
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)
|
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)
|
interval := calculateProgressInterval(show, false)
|
||||||
canUpdateStatus := stdoutCanUpdateStatus()
|
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
|
var status string
|
||||||
if max == 0 {
|
if max == 0 {
|
||||||
status = fmt.Sprintf("[%s] %d %s",
|
status = fmt.Sprintf("[%s] %d %s",
|
||||||
|
10
go.mod
10
go.mod
@@ -1,7 +1,7 @@
|
|||||||
module github.com/restic/restic
|
module github.com/restic/restic
|
||||||
|
|
||||||
require (
|
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/azcore v1.3.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1
|
||||||
github.com/anacrolix/fuse v0.2.0
|
github.com/anacrolix/fuse v0.2.0
|
||||||
@@ -12,9 +12,9 @@ require (
|
|||||||
github.com/google/go-cmp v0.5.9
|
github.com/google/go-cmp v0.5.9
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.1
|
github.com/hashicorp/golang-lru/v2 v2.0.1
|
||||||
github.com/juju/ratelimit v1.0.2
|
github.com/juju/ratelimit v1.0.2
|
||||||
github.com/klauspost/compress v1.15.14
|
github.com/klauspost/compress v1.15.15
|
||||||
github.com/kurin/blazer v0.5.4-0.20211030221322-ba894c124ac6
|
github.com/kurin/blazer v0.5.4-0.20230113224640-3887e1ec64b5
|
||||||
github.com/minio/minio-go/v7 v7.0.46
|
github.com/minio/minio-go/v7 v7.0.47
|
||||||
github.com/minio/sha256-simd v1.0.0
|
github.com/minio/sha256-simd v1.0.0
|
||||||
github.com/ncw/swift/v2 v2.0.1
|
github.com/ncw/swift/v2 v2.0.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
@@ -31,7 +31,7 @@ require (
|
|||||||
golang.org/x/sys v0.4.0
|
golang.org/x/sys v0.4.0
|
||||||
golang.org/x/term v0.4.0
|
golang.org/x/term v0.4.0
|
||||||
golang.org/x/text v0.6.0
|
golang.org/x/text v0.6.0
|
||||||
google.golang.org/api v0.106.0
|
google.golang.org/api v0.108.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI=
|
||||||
cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
|
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/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.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
|
||||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
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 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/azcore v1.3.0/go.mod h1:tZoQYdDZNOiIjdSn0dVWVfl0NEPGOJqVLzSrcFk4Is0=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
|
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/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 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
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.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
||||||
github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
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.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.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 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
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 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
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.20230113224640-3887e1ec64b5 h1:OUlGa6AAolmjyPtILbMJ8vHayz5wd4wBUloheGcMhfA=
|
||||||
github.com/kurin/blazer v0.5.4-0.20211030221322-ba894c124ac6/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU=
|
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/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 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
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.47 h1:sLiuCKGSIcn/MI6lREmTzX91DX/oRau4ia0j6e6eOSs=
|
||||||
github.com/minio/minio-go/v7 v7.0.46/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
|
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 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
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=
|
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-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 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
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.108.0 h1:WVBc/faN0DkKtR43Q/7+tPny9ZoLZdIiAyG5Q9vFClg=
|
||||||
google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
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.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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
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"
|
"runtime"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/restic/restic/internal/backend"
|
||||||
"github.com/restic/restic/internal/crypto"
|
"github.com/restic/restic/internal/crypto"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
@@ -30,11 +31,6 @@ func (c *Cache) canBeCached(t restic.FileType) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
type readCloser struct {
|
|
||||||
io.Reader
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load returns a reader that yields the contents of the file with the
|
// 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
|
// given handle. rd must be closed after use. If an error is returned, the
|
||||||
// ReadCloser is nil.
|
// 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 {
|
||||||
if length > 0 {
|
return f, nil
|
||||||
rd.Reader = io.LimitReader(f, int64(length))
|
|
||||||
}
|
}
|
||||||
|
return backend.LimitReadCloser(f, int64(length)), nil
|
||||||
return rd, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves a file in the cache.
|
// Save saves a file in the cache.
|
||||||
|
@@ -317,9 +317,9 @@ type blobJSON struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generatePackList returns a list of packs.
|
// generatePackList returns a list of packs.
|
||||||
func (idx *Index) generatePackList() ([]*packJSON, error) {
|
func (idx *Index) generatePackList() ([]packJSON, error) {
|
||||||
list := []*packJSON{}
|
list := make([]packJSON, 0, len(idx.packs))
|
||||||
packs := make(map[restic.ID]*packJSON)
|
packs := make(map[restic.ID]int, len(list)) // Maps to index in list.
|
||||||
|
|
||||||
for typ := range idx.byType {
|
for typ := range idx.byType {
|
||||||
m := &idx.byType[typ]
|
m := &idx.byType[typ]
|
||||||
@@ -329,18 +329,13 @@ func (idx *Index) generatePackList() ([]*packJSON, error) {
|
|||||||
panic("null pack id")
|
panic("null pack id")
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.Log("handle blob %v", e.id)
|
i, ok := packs[packID]
|
||||||
|
|
||||||
// see if pack is already in map
|
|
||||||
p, ok := packs[packID]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
// else create new pack
|
i = len(list)
|
||||||
p = &packJSON{ID: packID}
|
list = append(list, packJSON{ID: packID})
|
||||||
|
packs[packID] = i
|
||||||
// and append it to the list and map
|
|
||||||
list = append(list, p)
|
|
||||||
packs[p.ID] = p
|
|
||||||
}
|
}
|
||||||
|
p := &list[i]
|
||||||
|
|
||||||
// add blob
|
// add blob
|
||||||
p.Blobs = append(p.Blobs, blobJSON{
|
p.Blobs = append(p.Blobs, blobJSON{
|
||||||
@@ -355,14 +350,12 @@ func (idx *Index) generatePackList() ([]*packJSON, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.Log("done")
|
|
||||||
|
|
||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonIndex struct {
|
type jsonIndex struct {
|
||||||
Supersedes restic.IDs `json:"supersedes,omitempty"`
|
Supersedes restic.IDs `json:"supersedes,omitempty"`
|
||||||
Packs []*packJSON `json:"packs"`
|
Packs []packJSON `json:"packs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode writes the JSON serialization of the index to the writer w.
|
// Encode writes the JSON serialization of the index to the writer w.
|
||||||
|
@@ -3,6 +3,7 @@ package index_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"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) {
|
func TestIndexUnserializeOld(t *testing.T) {
|
||||||
idx, oldFormat, err := index.DecodeIndex(docOldExample, restic.NewRandomID())
|
idx, oldFormat, err := index.DecodeIndex(docOldExample, restic.NewRandomID())
|
||||||
rtest.OK(t, err)
|
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()}
|
h := restic.Handle{Type: t, Name: id.String()}
|
||||||
retriedInvalidData := false
|
retriedInvalidData := false
|
||||||
|
var dataErr error
|
||||||
err := r.be.Load(ctx, h, 0, 0, func(rd io.Reader) 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
|
// make sure this call is idempotent, in case an error occurs
|
||||||
wr := bytes.NewBuffer(buf[:0])
|
wr := bytes.NewBuffer(buf[:0])
|
||||||
@@ -202,13 +203,20 @@ func (r *Repository) LoadUnpacked(ctx context.Context, t restic.FileType, id res
|
|||||||
if !retriedInvalidData {
|
if !retriedInvalidData {
|
||||||
retriedInvalidData = true
|
retriedInvalidData = true
|
||||||
} else {
|
} 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()
|
cancel()
|
||||||
}
|
}
|
||||||
return errors.Errorf("load(%v): invalid data returned", h)
|
return restic.ErrInvalidData
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if dataErr != nil {
|
||||||
|
return nil, dataErr
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/backend/local"
|
"github.com/restic/restic/internal/backend/local"
|
||||||
"github.com/restic/restic/internal/backend/mem"
|
"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/crypto"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/test"
|
"github.com/restic/restic/internal/test"
|
||||||
@@ -97,11 +98,14 @@ func TestRepositoryWithVersion(t testing.TB, version uint) restic.Repository {
|
|||||||
|
|
||||||
// TestOpenLocal opens a local repository.
|
// TestOpenLocal opens a local repository.
|
||||||
func TestOpenLocal(t testing.TB, dir string) (r restic.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})
|
be, err := local.Open(context.TODO(), local.Config{Path: dir, Connections: 2})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
be = retry.New(be, 3, nil, nil)
|
||||||
|
|
||||||
repo, err := New(be, Options{})
|
repo, err := New(be, Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@@ -93,7 +93,7 @@ func TestFindUsedBlobs(t *testing.T) {
|
|||||||
snapshots = append(snapshots, sn)
|
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()
|
defer p.Done()
|
||||||
|
|
||||||
for i, sn := range snapshots {
|
for i, sn := range snapshots {
|
||||||
@@ -142,7 +142,7 @@ func TestMultiFindUsedBlobs(t *testing.T) {
|
|||||||
want.Merge(loadIDSet(t, goldenFilename))
|
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()
|
defer p.Done()
|
||||||
|
|
||||||
// run twice to check progress bar handling of duplicate tree roots
|
// 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)
|
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
|
// NewLock returns a new, non-exclusive lock for the repository. If an
|
||||||
// exclusive lock is already held by another process, it returns an error
|
// exclusive lock is already held by another process, it returns an error
|
||||||
// that satisfies IsAlreadyLocked.
|
// 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
|
// 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
|
// it could either be invalid or just unreadable due to network/permission problems
|
||||||
debug.Log("ignore lock %v: %v", id, err)
|
debug.Log("ignore lock %v: %v", id, err)
|
||||||
return errors.Fatal(err.Error())
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.Exclusive {
|
if l.Exclusive {
|
||||||
@@ -168,6 +189,9 @@ func (l *Lock) checkForOtherLocks(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, ErrInvalidData) {
|
||||||
|
return &invalidLockError{err}
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +222,7 @@ var StaleLockTimeout = 30 * time.Minute
|
|||||||
func (l *Lock) Stale() bool {
|
func (l *Lock) Stale() bool {
|
||||||
l.lock.Lock()
|
l.lock.Lock()
|
||||||
defer l.lock.Unlock()
|
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 {
|
if time.Since(l.Time) > StaleLockTimeout {
|
||||||
debug.Log("lock is stale, timestamp is too old: %v\n", l.Time)
|
debug.Log("lock is stale, timestamp is too old: %v\n", l.Time)
|
||||||
return true
|
return true
|
||||||
@@ -336,6 +360,12 @@ func ForAllLocks(ctx context.Context, repo Repository, excludeID *ID, fn func(ID
|
|||||||
if excludeID != nil && id.Equal(*excludeID) {
|
if excludeID != nil && id.Equal(*excludeID) {
|
||||||
return nil
|
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)
|
lock, err := LoadLock(ctx, repo, id)
|
||||||
|
|
||||||
m.Lock()
|
m.Lock()
|
||||||
|
@@ -4,10 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/crypto"
|
"github.com/restic/restic/internal/crypto"
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"golang.org/x/sync/errgroup"
|
"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
|
// Repository stores data in a backend. It provides high-level functions and
|
||||||
// transparently encrypts/decrypts data.
|
// transparently encrypts/decrypts data.
|
||||||
type Repository interface {
|
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"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rename (rather than remove) the running version. The running binary will be locked
|
// Rename (rather than remove) the running version. The running binary will be locked
|
||||||
// on Windows and cannot be removed while still executing.
|
// on Windows and cannot be removed while still executing.
|
||||||
func removeResticBinary(dir, target string) error {
|
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")
|
backup := filepath.Join(dir, filepath.Base(target)+".bak")
|
||||||
if _, err := os.Stat(backup); err == nil {
|
if _, err := os.Stat(backup); err == nil {
|
||||||
_ = os.Remove(backup)
|
_ = os.Remove(backup)
|
||||||
|
@@ -15,7 +15,6 @@ import (
|
|||||||
// JSONProgress reports progress for the `backup` command in JSON.
|
// JSONProgress reports progress for the `backup` command in JSON.
|
||||||
type JSONProgress struct {
|
type JSONProgress struct {
|
||||||
*ui.Message
|
*ui.Message
|
||||||
*ui.StdioWrapper
|
|
||||||
|
|
||||||
term *termstatus.Terminal
|
term *termstatus.Terminal
|
||||||
v uint
|
v uint
|
||||||
@@ -28,7 +27,6 @@ var _ ProgressPrinter = &JSONProgress{}
|
|||||||
func NewJSONProgress(term *termstatus.Terminal, verbosity uint) *JSONProgress {
|
func NewJSONProgress(term *termstatus.Terminal, verbosity uint) *JSONProgress {
|
||||||
return &JSONProgress{
|
return &JSONProgress{
|
||||||
Message: ui.NewMessage(term, verbosity),
|
Message: ui.NewMessage(term, verbosity),
|
||||||
StdioWrapper: ui.NewStdioWrapper(term),
|
|
||||||
term: term,
|
term: term,
|
||||||
v: verbosity,
|
v: verbosity,
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/archiver"
|
"github.com/restic/restic/internal/archiver"
|
||||||
"github.com/restic/restic/internal/restic"
|
"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.
|
// 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)
|
Finish(snapshotID restic.ID, start time.Time, summary *Summary, dryRun bool)
|
||||||
Reset()
|
Reset()
|
||||||
|
|
||||||
// ui.StdioWrapper
|
|
||||||
Stdout() io.WriteCloser
|
|
||||||
Stderr() io.WriteCloser
|
|
||||||
|
|
||||||
P(msg string, args ...interface{})
|
P(msg string, args ...interface{})
|
||||||
V(msg string, args ...interface{})
|
V(msg string, args ...interface{})
|
||||||
}
|
}
|
||||||
@@ -46,9 +40,9 @@ type Summary struct {
|
|||||||
|
|
||||||
// Progress reports progress for the `backup` command.
|
// Progress reports progress for the `backup` command.
|
||||||
type Progress struct {
|
type Progress struct {
|
||||||
|
progress.Updater
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
interval time.Duration
|
|
||||||
start time.Time
|
start time.Time
|
||||||
|
|
||||||
scanStarted, scanFinished bool
|
scanStarted, scanFinished bool
|
||||||
@@ -57,66 +51,37 @@ type Progress struct {
|
|||||||
processed, total Counter
|
processed, total Counter
|
||||||
errors uint
|
errors uint
|
||||||
|
|
||||||
closed chan struct{}
|
|
||||||
|
|
||||||
summary Summary
|
summary Summary
|
||||||
printer ProgressPrinter
|
printer ProgressPrinter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProgress(printer ProgressPrinter, interval time.Duration) *Progress {
|
func NewProgress(printer ProgressPrinter, interval time.Duration) *Progress {
|
||||||
return &Progress{
|
p := &Progress{
|
||||||
interval: interval,
|
|
||||||
start: time.Now(),
|
start: time.Now(),
|
||||||
|
|
||||||
currentFiles: make(map[string]struct{}),
|
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 {
|
||||||
// Run regularly updates the status lines. It should be called in a separate
|
p.printer.Reset()
|
||||||
// goroutine.
|
} else {
|
||||||
func (p *Progress) Run(ctx context.Context) {
|
|
||||||
defer close(p.closed)
|
|
||||||
// Reset status when finished
|
|
||||||
defer p.printer.Reset()
|
|
||||||
|
|
||||||
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.mu.Lock()
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
if !p.scanStarted {
|
if !p.scanStarted {
|
||||||
p.mu.Unlock()
|
return
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var secondsRemaining uint64
|
var secondsRemaining uint64
|
||||||
if p.scanFinished {
|
if p.scanFinished {
|
||||||
secs := float64(now.Sub(p.start) / time.Second)
|
secs := float64(runtime / time.Second)
|
||||||
todo := float64(p.total.Bytes - p.processed.Bytes)
|
todo := float64(p.total.Bytes - p.processed.Bytes)
|
||||||
secondsRemaining = uint64(secs / float64(p.processed.Bytes) * todo)
|
secondsRemaining = uint64(secs / float64(p.processed.Bytes) * todo)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.printer.Update(p.total, p.processed, p.errors, p.currentFiles, p.start, secondsRemaining)
|
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.
|
// 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
|
p.scanStarted = true
|
||||||
|
|
||||||
if item == "" {
|
if item == "" {
|
||||||
|
p.scanFinished = true
|
||||||
p.printer.ReportTotal(item, p.start, s)
|
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.
|
// Finish prints the finishing messages.
|
||||||
func (p *Progress) Finish(snapshotID restic.ID, dryrun bool) {
|
func (p *Progress) Finish(snapshotID restic.ID, dryrun bool) {
|
||||||
// wait for the status update goroutine to shut down
|
// wait for the status update goroutine to shut down
|
||||||
<-p.closed
|
p.Updater.Done()
|
||||||
p.printer.Finish(snapshotID, p.start, &p.summary, dryrun)
|
p.printer.Finish(snapshotID, p.start, &p.summary, dryrun)
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -45,9 +43,6 @@ func (p *mockPrinter) Finish(id restic.ID, _ time.Time, summary *Summary, dryRun
|
|||||||
|
|
||||||
func (p *mockPrinter) Reset() {}
|
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) P(msg string, args ...interface{}) {}
|
||||||
func (p *mockPrinter) V(msg string, args ...interface{}) {}
|
func (p *mockPrinter) V(msg string, args ...interface{}) {}
|
||||||
|
|
||||||
@@ -57,9 +52,6 @@ func TestProgress(t *testing.T) {
|
|||||||
prnt := &mockPrinter{}
|
prnt := &mockPrinter{}
|
||||||
prog := NewProgress(prnt, time.Millisecond)
|
prog := NewProgress(prnt, time.Millisecond)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
go prog.Run(ctx)
|
|
||||||
|
|
||||||
prog.StartFile("foo")
|
prog.StartFile("foo")
|
||||||
prog.CompleteBlob(1024)
|
prog.CompleteBlob(1024)
|
||||||
|
|
||||||
@@ -71,7 +63,6 @@ func TestProgress(t *testing.T) {
|
|||||||
prog.CompleteItem("foo", nil, &node, archiver.ItemStats{}, 0)
|
prog.CompleteItem("foo", nil, &node, archiver.ItemStats{}, 0)
|
||||||
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
cancel()
|
|
||||||
id := restic.NewRandomID()
|
id := restic.NewRandomID()
|
||||||
prog.Finish(id, false)
|
prog.Finish(id, false)
|
||||||
|
|
||||||
|
@@ -14,7 +14,6 @@ import (
|
|||||||
// TextProgress reports progress for the `backup` command.
|
// TextProgress reports progress for the `backup` command.
|
||||||
type TextProgress struct {
|
type TextProgress struct {
|
||||||
*ui.Message
|
*ui.Message
|
||||||
*ui.StdioWrapper
|
|
||||||
|
|
||||||
term *termstatus.Terminal
|
term *termstatus.Terminal
|
||||||
}
|
}
|
||||||
@@ -26,7 +25,6 @@ var _ ProgressPrinter = &TextProgress{}
|
|||||||
func NewTextProgress(term *termstatus.Terminal, verbosity uint) *TextProgress {
|
func NewTextProgress(term *termstatus.Terminal, verbosity uint) *TextProgress {
|
||||||
return &TextProgress{
|
return &TextProgress{
|
||||||
Message: ui.NewMessage(term, verbosity),
|
Message: ui.NewMessage(term, verbosity),
|
||||||
StdioWrapper: ui.NewStdioWrapper(term),
|
|
||||||
term: term,
|
term: term,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,9 +3,6 @@ package progress
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/debug"
|
|
||||||
"github.com/restic/restic/internal/ui/signals"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// A Func is a callback for a Counter.
|
// 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.
|
// The Func is also called when SIGUSR1 (or SIGINFO, on BSD) is received.
|
||||||
type Counter struct {
|
type Counter struct {
|
||||||
report Func
|
Updater
|
||||||
start time.Time
|
|
||||||
stopped chan struct{} // Closed by run.
|
|
||||||
stop chan struct{} // Close to stop run.
|
|
||||||
tick *time.Ticker
|
|
||||||
|
|
||||||
valueMutex sync.Mutex
|
valueMutex sync.Mutex
|
||||||
value uint64
|
value uint64
|
||||||
max uint64
|
max uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// New starts a new Counter.
|
// NewCounter starts a new Counter.
|
||||||
func New(interval time.Duration, total uint64, report Func) *Counter {
|
func NewCounter(interval time.Duration, total uint64, report Func) *Counter {
|
||||||
c := &Counter{
|
c := &Counter{
|
||||||
report: report,
|
|
||||||
start: time.Now(),
|
|
||||||
stopped: make(chan struct{}),
|
|
||||||
stop: make(chan struct{}),
|
|
||||||
max: total,
|
max: total,
|
||||||
}
|
}
|
||||||
|
c.Updater = *NewUpdater(interval, func(runtime time.Duration, final bool) {
|
||||||
if interval > 0 {
|
v, max := c.Get()
|
||||||
c.tick = time.NewTicker(interval)
|
report(v, max, runtime, final)
|
||||||
}
|
})
|
||||||
|
|
||||||
go c.run()
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,18 +56,6 @@ func (c *Counter) SetMax(max uint64) {
|
|||||||
c.valueMutex.Unlock()
|
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.
|
// Get returns the current value and the maximum of c.
|
||||||
// This method is concurrency-safe.
|
// This method is concurrency-safe.
|
||||||
func (c *Counter) Get() (v, max uint64) {
|
func (c *Counter) Get() (v, max uint64) {
|
||||||
@@ -91,32 +66,8 @@ func (c *Counter) Get() (v, max uint64) {
|
|||||||
return v, max
|
return v, max
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Counter) run() {
|
func (c *Counter) Done() {
|
||||||
defer close(c.stopped)
|
if c != nil {
|
||||||
defer func() {
|
c.Updater.Done()
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,7 +35,7 @@ func TestCounter(t *testing.T) {
|
|||||||
lastTotal = total
|
lastTotal = total
|
||||||
ncalls++
|
ncalls++
|
||||||
}
|
}
|
||||||
c := progress.New(10*time.Millisecond, startTotal, report)
|
c := progress.NewCounter(10*time.Millisecond, startTotal, report)
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
@@ -63,24 +63,6 @@ func TestCounterNil(t *testing.T) {
|
|||||||
// Shouldn't panic.
|
// Shouldn't panic.
|
||||||
var c *progress.Counter
|
var c *progress.Counter
|
||||||
c.Add(1)
|
c.Add(1)
|
||||||
|
c.SetMax(42)
|
||||||
c.Done()
|
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