diff --git a/internal/migrations/upgrade_repo_v2.go b/internal/migrations/upgrade_repo_v2.go index 6f4225947..23a7f1ff0 100644 --- a/internal/migrations/upgrade_repo_v2.go +++ b/internal/migrations/upgrade_repo_v2.go @@ -3,10 +3,8 @@ package migrations import ( "context" "fmt" - "os" - "path/filepath" - "github.com/restic/restic/internal/backend" + "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" ) @@ -14,26 +12,6 @@ func init() { 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{} func (*UpgradeRepoV2) Name() string { @@ -56,70 +34,7 @@ func (*UpgradeRepoV2) Check(_ context.Context, repo restic.Repository) (bool, st func (*UpgradeRepoV2) RepoCheck() bool { 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 { - 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 = 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 + return repository.UpgradeRepo(ctx, repo.(*repository.Repository)) } diff --git a/internal/migrations/upgrade_repo_v2_test.go b/internal/migrations/upgrade_repo_v2_test.go index 845d20e92..59f2394e0 100644 --- a/internal/migrations/upgrade_repo_v2_test.go +++ b/internal/migrations/upgrade_repo_v2_test.go @@ -2,15 +2,9 @@ package migrations import ( "context" - "os" - "path/filepath" - "sync" "testing" - "github.com/restic/restic/internal/backend" - "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/test" ) func TestUpgradeRepoV2(t *testing.T) { @@ -35,73 +29,3 @@ func TestUpgradeRepoV2(t *testing.T) { 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))) -} diff --git a/internal/repository/upgrade_repo.go b/internal/repository/upgrade_repo.go new file mode 100644 index 000000000..3e86cc377 --- /dev/null +++ b/internal/repository/upgrade_repo.go @@ -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 +} diff --git a/internal/repository/upgrade_repo_test.go b/internal/repository/upgrade_repo_test.go new file mode 100644 index 000000000..47c5f856c --- /dev/null +++ b/internal/repository/upgrade_repo_test.go @@ -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))) +}