package recovery import ( "context" "errors" "fmt" "log/slog" "github.com/google/uuid" "github.com/syncova/syncova/packages/agent" "github.com/syncova/syncova/packages/backupengine" "github.com/syncova/syncova/packages/jobs" "github.com/syncova/syncova/packages/platform/crypto" "github.com/syncova/syncova/packages/platform/logging" "github.com/syncova/syncova/packages/repository" ) // RepositoryResolver liefert Repository und Backupkennung zu einem Backup. // // Die Schnittstelle haelt das Wiederherstellungspaket frei von der // Auftragsverwaltung: Wo ein Backup liegt, weiss die Control Plane, nicht die // Wiederherstellung. type RepositoryResolver interface { // ResolveBackup liefert Pfad und Kennung eines Backups. ResolveBackup(resolveContext context.Context, backupIdentifier uuid.UUID) (repositoryPath string, backupIDInRepository string, resolveError error) } // StoreRepositoryResolver loest Backups ueber die Auftragsverwaltung auf. type StoreRepositoryResolver struct { // jobStore ist die Datenzugriffsschicht der Auftraege. jobStore *jobs.PostgresStore } // NewStoreRepositoryResolver erzeugt die Aufloesung. func NewStoreRepositoryResolver(jobStore *jobs.PostgresStore) *StoreRepositoryResolver { return &StoreRepositoryResolver{jobStore: jobStore} } // ResolveBackup liefert Pfad und Kennung eines Backups. func (resolver *StoreRepositoryResolver) ResolveBackup(resolveContext context.Context, backupIdentifier uuid.UUID) (string, string, error) { backupRecord, readError := resolver.jobStore.GetBackup(resolveContext, backupIdentifier) if readError != nil { return "", "", readError } repositoryRecord, repositoryError := resolver.jobStore.GetRepository(resolveContext, backupRecord.RepositoryID) if repositoryError != nil { return "", "", repositoryError } return repositoryRecord.Location, backupRecord.BackupIDInRepository, nil } // ExecutorOptions steuern den Wiederherstellungs-Executor. type ExecutorOptions struct { // SecretStore entschluesselt die Datenschluessel der Repositories. SecretStore crypto.SecretStore } // RestoreExecutor fuehrt Wiederherstellungen ueber die Backup Engine aus. type RestoreExecutor struct { // resolver loest Backups auf ihr Repository auf. resolver RepositoryResolver // options sind die Einstellungen. options ExecutorOptions // logger protokolliert den Verlauf. logger *slog.Logger } // Sicherstellen, dass die Schnittstelle erfuellt wird. var _ Executor = (*RestoreExecutor)(nil) // NewRestoreExecutor erzeugt den Executor. func NewRestoreExecutor(resolver RepositoryResolver, executorOptions ExecutorOptions, baseLogger *slog.Logger) (*RestoreExecutor, error) { if resolver == nil { return nil, errors.New("der executor braucht eine aufloesung fuer backups") } return &RestoreExecutor{ resolver: resolver, options: executorOptions, logger: logging.WithComponent(baseLogger, "restore-executor"), }, nil } // Execute fuehrt eine Wiederherstellung aus. // // Das Repository wird **schreibgeschuetzt** geoeffnet: Eine Wiederherstellung // liest nur. Damit kann sie neben einer laufenden Sicherung stattfinden, statt // auf deren Schreibsperre zu warten — und im Ernstfall wartet niemand gern. func (executor *RestoreExecutor) Execute(executionContext context.Context, executionRequest ExecutionRequest) (ExecutionResult, error) { restoreJob := executionRequest.Job restoreLogger := executor.logger.With( slog.String("restore_id", restoreJob.ID.String()), slog.String("ziel", restoreJob.TargetRef)) repositoryPath, backupIDInRepository, resolveError := executor.resolver.ResolveBackup( executionContext, restoreJob.BackupID) if resolveError != nil { return ExecutionResult{}, &ExecutionError{ Code: "BACKUP_UNKNOWN", Message: "Das wiederherzustellende Backup ist nicht auffindbar.", Cause: resolveError, } } openedRepository, openError := repository.Open(executionContext, repositoryPath, repository.OpenOptions{ReadOnly: true}, restoreLogger) if openError != nil { return ExecutionResult{}, &ExecutionError{ Code: "REPOSITORY_UNAVAILABLE", Message: fmt.Sprintf("Das Repository unter %s liess sich nicht oeffnen.", repositoryPath), Cause: openError, } } defer func() { _ = openedRepository.Close() }() // Die Vorabpruefung laeuft **erneut**, unmittelbar vor dem Schreiben. Der // Bericht am Auftrag kann Minuten oder Tage alt sein; in der Zwischenzeit // kann ein Block verschwunden oder das Ziel gefuellt worden sein. validator := NewValidator(openedRepository) validationReport, validationError := validator.Validate(executionContext, ValidationRequest{ BackupID: backupIDInRepository, TargetPath: restoreJob.TargetRef, PathPrefix: restoreJob.PathPrefix, OverwriteExisting: restoreJob.OverwriteExisting || executionRequest.ResumeAfterPath != "", DeepChunkCheck: false, }) if validationError != nil { return ExecutionResult{}, &ExecutionError{ Code: "VALIDATION_FAILED", Message: "Die Vorabpruefung liess sich nicht ausfuehren.", Cause: validationError, } } if !validationReport.CanProceed() { blockingFindings := validationReport.BlockingFindings() return ExecutionResult{}, &ExecutionError{ Code: "PRECONDITION_FAILED", Message: fmt.Sprintf("Die Wiederherstellung wurde vor dem Schreiben abgelehnt: %s", blockingFindings[0].Message), } } engine := backupengine.NewEngine(openedRepository, executor.options.SecretStore, restoreLogger) restoreRunner := agent.NewRestoreRunner(engine, restoreLogger) runResult, restoreError := restoreRunner.RunRestore(executionContext, agent.RestoreRunOptions{ BackupID: backupIDInRepository, TargetPath: restoreJob.TargetRef, PathPrefix: restoreJob.PathPrefix, OverwriteExisting: restoreJob.OverwriteExisting, RestorePermissions: restoreJob.RestorePermissions, ResumeAfterPath: executionRequest.ResumeAfterPath, EntryRestoredCallback: executionRequest.EntryRestoredCallback, }) if restoreError != nil { return ExecutionResult{}, &ExecutionError{ Code: "RESTORE_FAILED", Message: restoreError.Error(), Cause: restoreError, } } executionResult := ExecutionResult{ BytesRestored: runResult.BytesRestored, FilesRestored: int64(runResult.FilesRestored + runResult.DirectoriesCreated + runResult.SymlinksCreated), FilesSkipped: int64(runResult.SkippedExisting), } if runResult.SkippedExisting > 0 { // Ein uebergangenes vorhandenes Objekt bedeutet: Die Wiederherstellung // ist unvollstaendig. Der Anwender glaubte sonst, alles sei wieder da. executionResult.SkipReasons = append(executionResult.SkipReasons, fmt.Sprintf("%d vorhandene Objekte wurden nicht ueberschrieben.", runResult.SkippedExisting)) } restoreLogger.Info("wiederherstellung ausgefuehrt", slog.Int("dateien", runResult.FilesRestored), slog.Int("verzeichnisse", runResult.DirectoriesCreated), slog.Int("geprueft", runResult.VerifiedFiles), slog.Int("uebersprungen_fortsetzung", runResult.SkippedResumed)) return executionResult, nil }