mirror of
https://github.com/restic/restic.git
synced 2025-01-03 05:35:43 +00:00
migrations: move logic of upgrade_repo_v2 to repository package
The migration modifies repository internals and thus should live within the repository package.
This commit is contained in:
parent
ab9077bc13
commit
34d90aecf9
4 changed files with 187 additions and 163 deletions
|
@ -3,10 +3,8 @@ package migrations
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,26 +12,6 @@ func init() {
|
||||||
register(&UpgradeRepoV2{})
|
register(&UpgradeRepoV2{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpgradeRepoV2Error struct {
|
|
||||||
UploadNewConfigError error
|
|
||||||
ReuploadOldConfigError error
|
|
||||||
|
|
||||||
BackupFilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *UpgradeRepoV2Error) Error() string {
|
|
||||||
if err.ReuploadOldConfigError != nil {
|
|
||||||
return fmt.Sprintf("error uploading config (%v), re-uploading old config filed failed as well (%v), but there is a backup of the config file in %v", err.UploadNewConfigError, err.ReuploadOldConfigError, err.BackupFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("error uploading config (%v), re-uploaded old config was successful, there is a backup of the config file in %v", err.UploadNewConfigError, err.BackupFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *UpgradeRepoV2Error) Unwrap() error {
|
|
||||||
// consider the original upload error as the primary cause
|
|
||||||
return err.UploadNewConfigError
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpgradeRepoV2 struct{}
|
type UpgradeRepoV2 struct{}
|
||||||
|
|
||||||
func (*UpgradeRepoV2) Name() string {
|
func (*UpgradeRepoV2) Name() string {
|
||||||
|
@ -56,70 +34,7 @@ func (*UpgradeRepoV2) Check(_ context.Context, repo restic.Repository) (bool, st
|
||||||
func (*UpgradeRepoV2) RepoCheck() bool {
|
func (*UpgradeRepoV2) RepoCheck() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
func (*UpgradeRepoV2) upgrade(ctx context.Context, repo restic.Repository) error {
|
|
||||||
h := backend.Handle{Type: backend.ConfigFile}
|
|
||||||
|
|
||||||
if !repo.Backend().HasAtomicReplace() {
|
|
||||||
// remove the original file for backends which do not support atomic overwriting
|
|
||||||
err := repo.Backend().Remove(ctx, h)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("remove config failed: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// upgrade config
|
|
||||||
cfg := repo.Config()
|
|
||||||
cfg.Version = 2
|
|
||||||
|
|
||||||
err := restic.SaveConfig(ctx, repo, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("save new config file failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *UpgradeRepoV2) Apply(ctx context.Context, repo restic.Repository) error {
|
func (m *UpgradeRepoV2) Apply(ctx context.Context, repo restic.Repository) error {
|
||||||
tempdir, err := os.MkdirTemp("", "restic-migrate-upgrade-repo-v2-")
|
return repository.UpgradeRepo(ctx, repo.(*repository.Repository))
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create temp dir failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
h := backend.Handle{Type: restic.ConfigFile}
|
|
||||||
|
|
||||||
// read raw config file and save it to a temp dir, just in case
|
|
||||||
rawConfigFile, err := repo.LoadRaw(ctx, restic.ConfigFile, restic.ID{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("load config file failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
backupFileName := filepath.Join(tempdir, "config")
|
|
||||||
err = os.WriteFile(backupFileName, rawConfigFile, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("write config file backup to %v failed: %w", tempdir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// run the upgrade
|
|
||||||
err = m.upgrade(ctx, repo)
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
// build an error we can return to the caller
|
|
||||||
repoError := &UpgradeRepoV2Error{
|
|
||||||
UploadNewConfigError: err,
|
|
||||||
BackupFilePath: backupFileName,
|
|
||||||
}
|
|
||||||
|
|
||||||
// try contingency methods, reupload the original file
|
|
||||||
_ = repo.Backend().Remove(ctx, h)
|
|
||||||
err = repo.Backend().Save(ctx, h, backend.NewByteReader(rawConfigFile, nil))
|
|
||||||
if err != nil {
|
|
||||||
repoError.ReuploadOldConfigError = err
|
|
||||||
}
|
|
||||||
|
|
||||||
return repoError
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = os.Remove(backupFileName)
|
|
||||||
_ = os.Remove(tempdir)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,9 @@ package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend"
|
|
||||||
"github.com/restic/restic/internal/errors"
|
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUpgradeRepoV2(t *testing.T) {
|
func TestUpgradeRepoV2(t *testing.T) {
|
||||||
|
@ -35,73 +29,3 @@ func TestUpgradeRepoV2(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type failBackend struct {
|
|
||||||
backend.Backend
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
ConfigFileSavesUntilError uint
|
|
||||||
}
|
|
||||||
|
|
||||||
func (be *failBackend) Save(ctx context.Context, h backend.Handle, rd backend.RewindReader) error {
|
|
||||||
if h.Type != backend.ConfigFile {
|
|
||||||
return be.Backend.Save(ctx, h, rd)
|
|
||||||
}
|
|
||||||
|
|
||||||
be.mu.Lock()
|
|
||||||
if be.ConfigFileSavesUntilError == 0 {
|
|
||||||
be.mu.Unlock()
|
|
||||||
return errors.New("failure induced for testing")
|
|
||||||
}
|
|
||||||
|
|
||||||
be.ConfigFileSavesUntilError--
|
|
||||||
be.mu.Unlock()
|
|
||||||
|
|
||||||
return be.Backend.Save(ctx, h, rd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpgradeRepoV2Failure(t *testing.T) {
|
|
||||||
be := repository.TestBackend(t)
|
|
||||||
|
|
||||||
// wrap backend so that it fails upgrading the config after the initial write
|
|
||||||
be = &failBackend{
|
|
||||||
ConfigFileSavesUntilError: 1,
|
|
||||||
Backend: be,
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := repository.TestRepositoryWithBackend(t, be, 1, repository.Options{})
|
|
||||||
if repo.Config().Version != 1 {
|
|
||||||
t.Fatal("test repo has wrong version")
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &UpgradeRepoV2{}
|
|
||||||
|
|
||||||
ok, _, err := m.Check(context.Background(), repo)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("migration check returned false")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = m.Apply(context.Background(), repo)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error returned from Apply(), got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
upgradeErr := err.(*UpgradeRepoV2Error)
|
|
||||||
if upgradeErr.UploadNewConfigError == nil {
|
|
||||||
t.Fatal("expected upload error, got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if upgradeErr.ReuploadOldConfigError == nil {
|
|
||||||
t.Fatal("expected reupload error, got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if upgradeErr.BackupFilePath == "" {
|
|
||||||
t.Fatal("no backup file path found")
|
|
||||||
}
|
|
||||||
test.OK(t, os.Remove(upgradeErr.BackupFilePath))
|
|
||||||
test.OK(t, os.Remove(filepath.Dir(upgradeErr.BackupFilePath)))
|
|
||||||
}
|
|
||||||
|
|
103
internal/repository/upgrade_repo.go
Normal file
103
internal/repository/upgrade_repo.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/backend"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type upgradeRepoV2Error struct {
|
||||||
|
UploadNewConfigError error
|
||||||
|
ReuploadOldConfigError error
|
||||||
|
|
||||||
|
BackupFilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *upgradeRepoV2Error) Error() string {
|
||||||
|
if err.ReuploadOldConfigError != nil {
|
||||||
|
return fmt.Sprintf("error uploading config (%v), re-uploading old config filed failed as well (%v), but there is a backup of the config file in %v", err.UploadNewConfigError, err.ReuploadOldConfigError, err.BackupFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("error uploading config (%v), re-uploaded old config was successful, there is a backup of the config file in %v", err.UploadNewConfigError, err.BackupFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *upgradeRepoV2Error) Unwrap() error {
|
||||||
|
// consider the original upload error as the primary cause
|
||||||
|
return err.UploadNewConfigError
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeRepository(ctx context.Context, repo *Repository) error {
|
||||||
|
h := backend.Handle{Type: backend.ConfigFile}
|
||||||
|
|
||||||
|
if !repo.be.HasAtomicReplace() {
|
||||||
|
// remove the original file for backends which do not support atomic overwriting
|
||||||
|
err := repo.be.Remove(ctx, h)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove config failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgrade config
|
||||||
|
cfg := repo.Config()
|
||||||
|
cfg.Version = 2
|
||||||
|
|
||||||
|
err := restic.SaveConfig(ctx, repo, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("save new config file failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpgradeRepo(ctx context.Context, repo *Repository) error {
|
||||||
|
if repo.Config().Version != 1 {
|
||||||
|
return fmt.Errorf("repository has version %v, only upgrades from version 1 are supported", repo.Config().Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempdir, err := os.MkdirTemp("", "restic-migrate-upgrade-repo-v2-")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create temp dir failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := backend.Handle{Type: restic.ConfigFile}
|
||||||
|
|
||||||
|
// read raw config file and save it to a temp dir, just in case
|
||||||
|
rawConfigFile, err := repo.LoadRaw(ctx, restic.ConfigFile, restic.ID{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load config file failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupFileName := filepath.Join(tempdir, "config")
|
||||||
|
err = os.WriteFile(backupFileName, rawConfigFile, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write config file backup to %v failed: %w", tempdir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the upgrade
|
||||||
|
err = upgradeRepository(ctx, repo)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
// build an error we can return to the caller
|
||||||
|
repoError := &upgradeRepoV2Error{
|
||||||
|
UploadNewConfigError: err,
|
||||||
|
BackupFilePath: backupFileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// try contingency methods, reupload the original file
|
||||||
|
_ = repo.Backend().Remove(ctx, h)
|
||||||
|
err = repo.Backend().Save(ctx, h, backend.NewByteReader(rawConfigFile, nil))
|
||||||
|
if err != nil {
|
||||||
|
repoError.ReuploadOldConfigError = err
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoError
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = os.Remove(backupFileName)
|
||||||
|
_ = os.Remove(tempdir)
|
||||||
|
return nil
|
||||||
|
}
|
82
internal/repository/upgrade_repo_test.go
Normal file
82
internal/repository/upgrade_repo_test.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/backend"
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpgradeRepoV2(t *testing.T) {
|
||||||
|
repo := TestRepositoryWithVersion(t, 1)
|
||||||
|
if repo.Config().Version != 1 {
|
||||||
|
t.Fatal("test repo has wrong version")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := UpgradeRepo(context.Background(), repo.(*Repository))
|
||||||
|
rtest.OK(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type failBackend struct {
|
||||||
|
backend.Backend
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
ConfigFileSavesUntilError uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (be *failBackend) Save(ctx context.Context, h backend.Handle, rd backend.RewindReader) error {
|
||||||
|
if h.Type != backend.ConfigFile {
|
||||||
|
return be.Backend.Save(ctx, h, rd)
|
||||||
|
}
|
||||||
|
|
||||||
|
be.mu.Lock()
|
||||||
|
if be.ConfigFileSavesUntilError == 0 {
|
||||||
|
be.mu.Unlock()
|
||||||
|
return errors.New("failure induced for testing")
|
||||||
|
}
|
||||||
|
|
||||||
|
be.ConfigFileSavesUntilError--
|
||||||
|
be.mu.Unlock()
|
||||||
|
|
||||||
|
return be.Backend.Save(ctx, h, rd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpgradeRepoV2Failure(t *testing.T) {
|
||||||
|
be := TestBackend(t)
|
||||||
|
|
||||||
|
// wrap backend so that it fails upgrading the config after the initial write
|
||||||
|
be = &failBackend{
|
||||||
|
ConfigFileSavesUntilError: 1,
|
||||||
|
Backend: be,
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := TestRepositoryWithBackend(t, be, 1, Options{})
|
||||||
|
if repo.Config().Version != 1 {
|
||||||
|
t.Fatal("test repo has wrong version")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := UpgradeRepo(context.Background(), repo.(*Repository))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error returned from Apply(), got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
upgradeErr := err.(*upgradeRepoV2Error)
|
||||||
|
if upgradeErr.UploadNewConfigError == nil {
|
||||||
|
t.Fatal("expected upload error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if upgradeErr.ReuploadOldConfigError == nil {
|
||||||
|
t.Fatal("expected reupload error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if upgradeErr.BackupFilePath == "" {
|
||||||
|
t.Fatal("no backup file path found")
|
||||||
|
}
|
||||||
|
rtest.OK(t, os.Remove(upgradeErr.BackupFilePath))
|
||||||
|
rtest.OK(t, os.Remove(filepath.Dir(upgradeErr.BackupFilePath)))
|
||||||
|
}
|
Loading…
Reference in a new issue