package migrations import ( "context" "fmt" "io" "io/ioutil" "os" "path/filepath" "github.com/restic/restic/internal/restic" ) 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 { return "upgrade_repo_v2" } func (*UpgradeRepoV2) Desc() string { return "upgrade a repository to version 2" } func (*UpgradeRepoV2) Check(ctx context.Context, repo restic.Repository) (bool, error) { isV1 := repo.Config().Version == 1 return isV1, nil } func (*UpgradeRepoV2) RepoCheckOptions() *RepositoryCheckOptions { return &RepositoryCheckOptions{} } func (*UpgradeRepoV2) upgrade(ctx context.Context, repo restic.Repository) error { h := restic.Handle{Type: restic.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 := repo.SaveJSONUnpacked(ctx, restic.ConfigFile, 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 := ioutil.TempDir("", "restic-migrate-upgrade-repo-v2-") if err != nil { return fmt.Errorf("create temp dir failed: %w", err) } h := restic.Handle{Type: restic.ConfigFile} // read raw config file and save it to a temp dir, just in case var rawConfigFile []byte err = repo.Backend().Load(ctx, h, 0, 0, func(rd io.Reader) (err error) { rawConfigFile, err = ioutil.ReadAll(rd) return err }) if err != nil { return fmt.Errorf("load config file failed: %w", err) } backupFileName := filepath.Join(tempdir, "config") err = ioutil.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, restic.NewByteReader(rawConfigFile, nil)) if err != nil { repoError.ReuploadOldConfigError = err } return repoError } _ = os.Remove(backupFileName) _ = os.Remove(tempdir) return nil }