Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
460 lines
18 KiB
Go
460 lines
18 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/syncova/syncova/packages/audit"
|
|
"github.com/syncova/syncova/packages/auth"
|
|
"github.com/syncova/syncova/packages/jobs"
|
|
"github.com/syncova/syncova/packages/platform/logging"
|
|
"github.com/syncova/syncova/packages/verification"
|
|
)
|
|
|
|
// verificationHandler bedient die Pruefung (SYNCOVA_API.md §14).
|
|
type verificationHandler struct {
|
|
// verificationStore ist die Datenzugriffsschicht der Pruefauftraege.
|
|
verificationStore *verification.Store
|
|
// jobStore liefert Backups und Repositories.
|
|
jobStore *jobs.PostgresStore
|
|
// auditRecorder protokolliert ausgeloeste Pruefungen.
|
|
auditRecorder audit.Recorder
|
|
// logger protokolliert technische Fehler.
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// verificationRequest ist der Rumpf von POST /verification.
|
|
type verificationRequest struct {
|
|
// BackupID ist das zu pruefende Backup.
|
|
BackupID uuid.UUID `json:"backup_id"`
|
|
// VerificationType ist die Art der Pruefung.
|
|
VerificationType string `json:"verification_type"`
|
|
}
|
|
|
|
// verificationResponse ist die Darstellung eines Pruefauftrags.
|
|
type verificationResponse struct {
|
|
// ID ist der oeffentliche Bezeichner.
|
|
ID uuid.UUID `json:"id"`
|
|
// BackupID ist das gepruefte Backup.
|
|
BackupID uuid.UUID `json:"backup_id"`
|
|
// VerificationType ist die Art der Pruefung.
|
|
VerificationType string `json:"verification_type"`
|
|
// Status ist der Zustand.
|
|
Status string `json:"status"`
|
|
// Result ist das Ergebnis; leer solange nicht abgeschlossen.
|
|
Result string `json:"result,omitempty"`
|
|
// ChunksChecked ist die Zahl gepruefter Bloecke.
|
|
ChunksChecked int64 `json:"chunks_checked"`
|
|
// ChunksMissing ist die Zahl fehlender Bloecke.
|
|
ChunksMissing int64 `json:"chunks_missing"`
|
|
// ChunksCorrupted ist die Zahl beschaedigter Bloecke.
|
|
ChunksCorrupted int64 `json:"chunks_corrupted"`
|
|
// BytesRead ist die gelesene Datenmenge.
|
|
BytesRead int64 `json:"bytes_read"`
|
|
// StartedAt ist der Beginn in UTC.
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
// CompletedAt ist das Ende in UTC.
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
// DurationSeconds ist die Dauer in Sekunden.
|
|
DurationSeconds float64 `json:"duration_seconds,omitempty"`
|
|
// ErrorMessage ist die verstaendliche Fehlermeldung.
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
// Summary fasst das Ergebnis in einem Satz zusammen.
|
|
//
|
|
// Die Zusammenfassung sagt ausdruecklich, was **nicht** geprueft wurde: Eine
|
|
// Manifestpruefung ohne diesen Zusatz liesse sich fuer einen Nachweis der
|
|
// Wiederherstellbarkeit halten, der sie nicht ist.
|
|
Summary string `json:"summary,omitempty"`
|
|
// CorrelationID verbindet den Auftrag mit seinen Protokollzeilen.
|
|
CorrelationID uuid.UUID `json:"correlation_id"`
|
|
// CreatedAt ist der Anlagezeitpunkt in UTC.
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// assuranceResponse ist die Bewertung eines Backups.
|
|
type assuranceResponse struct {
|
|
// BackupID ist das bewertete Backup.
|
|
BackupID uuid.UUID `json:"backup_id"`
|
|
// Classification ist die objektive Einstufung.
|
|
Classification string `json:"classification"`
|
|
// ClassificationDescription erklaert die Einstufung.
|
|
ClassificationDescription string `json:"classification_description"`
|
|
// Percentage ist die Bewertung in Prozent.
|
|
Percentage int `json:"percentage"`
|
|
// UnknownInputCount ist die Zahl ungemessener Eingangsgroessen.
|
|
UnknownInputCount int `json:"unknown_input_count"`
|
|
// IsTrustworthy meldet eine belastbare Bewertung.
|
|
IsTrustworthy bool `json:"is_trustworthy"`
|
|
// Summary fasst die Bewertung in einem Satz zusammen.
|
|
Summary string `json:"summary"`
|
|
// MissingMeasurements nennt die fehlenden Messungen als Handlungsanweisung.
|
|
MissingMeasurements []string `json:"missing_measurements"`
|
|
// Inputs sind die einzelnen Eingangsgroessen.
|
|
Inputs []verification.ScoreInput `json:"inputs"`
|
|
// LastVerifiedAt ist die letzte Integritaetspruefung in UTC.
|
|
LastVerifiedAt *time.Time `json:"last_verified_at,omitempty"`
|
|
// LastRestoreTestAt ist der letzte Wiederherstellungstest in UTC.
|
|
LastRestoreTestAt *time.Time `json:"last_restore_test_at,omitempty"`
|
|
}
|
|
|
|
// handleCreateVerification bedient POST /verification.
|
|
//
|
|
// Die Pruefung wird eingereiht, nicht ausgefuehrt: Die Pruefschleife holt sie im
|
|
// naechsten Durchgang. Deshalb 202 statt 201 — das Ergebnis steht noch aus.
|
|
func (handler *verificationHandler) handleCreateVerification(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
actingUser, _ := AuthenticatedUserFromContext(request.Context())
|
|
|
|
var verificationPayload verificationRequest
|
|
if decodeError := decodeJSONBody(request, &verificationPayload); decodeError != nil {
|
|
WriteError(responseWriter, request, requestLogger, decodeError)
|
|
return
|
|
}
|
|
|
|
requestedType, typeError := parseVerificationType(verificationPayload.VerificationType)
|
|
if typeError != nil {
|
|
WriteError(responseWriter, request, requestLogger, typeError)
|
|
return
|
|
}
|
|
|
|
// Ein Wiederherstellungstest liest das gesamte Backup und schreibt es
|
|
// versuchsweise zurueck. Er belastet Datentraeger und Leitung erheblich und
|
|
// haengt deshalb an einem eigenen Recht.
|
|
if requestedType == verification.TypeRestoreTest && !userHasPermission(actingUser, "verification.restore_test") {
|
|
permissionError := NewValidationError(
|
|
"Fuer einen Wiederherstellungstest fehlt die Berechtigung verification.restore_test.")
|
|
permissionError.Code = ErrorCodePermissionDenied
|
|
permissionError.StatusCode = http.StatusForbidden
|
|
|
|
WriteError(responseWriter, request, requestLogger, permissionError)
|
|
|
|
return
|
|
}
|
|
|
|
// Das Backup wird vor dem Einreihen aufgeloest: Eine Pruefung eines nicht
|
|
// vorhandenen Backups liefe erst in der Schleife auf, wo niemand die Antwort
|
|
// sieht.
|
|
backupRecord, backupError := handler.jobStore.GetBackup(request.Context(), verificationPayload.BackupID)
|
|
if backupError != nil {
|
|
if errors.Is(backupError, jobs.ErrBackupNotFound) {
|
|
WriteError(responseWriter, request, requestLogger,
|
|
NewNotFoundError("Das Backup wurde nicht gefunden."))
|
|
|
|
return
|
|
}
|
|
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(backupError))
|
|
|
|
return
|
|
}
|
|
|
|
createdJob, createError := handler.verificationStore.CreateJob(request.Context(),
|
|
verificationPayload.BackupID, requestedType, &actingUser.ID)
|
|
if createError != nil {
|
|
WriteError(responseWriter, request, requestLogger, translateVerificationError(createError))
|
|
return
|
|
}
|
|
|
|
handler.recordAudit(request, actingUser, audit.ActionVerificationRequested, verificationPayload.BackupID,
|
|
map[string]any{
|
|
"verification_id": createdJob.ID.String(),
|
|
"verification_type": string(requestedType),
|
|
"backup_in_repository": backupRecord.BackupIDInRepository,
|
|
})
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusAccepted, buildVerificationResponse(createdJob))
|
|
}
|
|
|
|
// handleListVerifications bedient GET /verification.
|
|
func (handler *verificationHandler) handleListVerifications(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
requestedPage := parsePositiveInteger(request.URL.Query().Get("page"), 1)
|
|
requestedPageSize := parsePositiveInteger(request.URL.Query().Get("page_size"), 50)
|
|
|
|
loadedJobs, totalCount, listError := handler.verificationStore.ListJobs(
|
|
request.Context(), requestedPage, requestedPageSize)
|
|
if listError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(listError))
|
|
return
|
|
}
|
|
|
|
jobResponses := make([]verificationResponse, 0, len(loadedJobs))
|
|
for jobIndex := range loadedJobs {
|
|
jobResponses = append(jobResponses, buildVerificationResponse(&loadedJobs[jobIndex]))
|
|
}
|
|
|
|
WritePaginatedSuccess(responseWriter, request, jobResponses, PaginationMeta{
|
|
Page: requestedPage,
|
|
PageSize: requestedPageSize,
|
|
Total: int64(totalCount),
|
|
})
|
|
}
|
|
|
|
// handleGetVerification bedient GET /verification/{id}.
|
|
func (handler *verificationHandler) handleGetVerification(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
jobIdentifier, parseError := parseVerificationIdentifier(request)
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, parseError)
|
|
return
|
|
}
|
|
|
|
loadedJob, readError := handler.verificationStore.GetJob(request.Context(), jobIdentifier)
|
|
if readError != nil {
|
|
WriteError(responseWriter, request, requestLogger, translateVerificationError(readError))
|
|
return
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, buildVerificationResponse(loadedJob))
|
|
}
|
|
|
|
// handleGetVerificationResults bedient GET /verification/{id}/results.
|
|
//
|
|
// Getrennt vom Auftrag, weil der vollstaendige Bericht bei einem grossen Backup
|
|
// viele Befunde traegt. Eine Liste von Auftraegen bliebe damit nicht mehr
|
|
// ueberschaubar.
|
|
func (handler *verificationHandler) handleGetVerificationResults(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
jobIdentifier, parseError := parseVerificationIdentifier(request)
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, parseError)
|
|
return
|
|
}
|
|
|
|
loadedJob, readError := handler.verificationStore.GetJob(request.Context(), jobIdentifier)
|
|
if readError != nil {
|
|
WriteError(responseWriter, request, requestLogger, translateVerificationError(readError))
|
|
return
|
|
}
|
|
|
|
if loadedJob.Report == nil {
|
|
// Kein Bericht heisst: Die Pruefung ist nicht so weit gekommen. Ein leeres
|
|
// Ergebnis auszugeben liesse das wie ein sauberes Ergebnis aussehen.
|
|
WriteError(responseWriter, request, requestLogger, NewNotFoundError(
|
|
"Fuer diese Pruefung liegt kein Bericht vor. Sie laeuft noch oder konnte nicht durchgefuehrt werden."))
|
|
|
|
return
|
|
}
|
|
|
|
resultPayload := map[string]any{
|
|
"verification_id": loadedJob.ID,
|
|
"backup_id": loadedJob.BackupID,
|
|
"verification_type": string(loadedJob.VerificationType),
|
|
"result": string(loadedJob.Result),
|
|
"summary": loadedJob.Report.Summary(),
|
|
"report": loadedJob.Report,
|
|
}
|
|
|
|
if loadedJob.RestoreTestReport != nil {
|
|
resultPayload["restore_test"] = loadedJob.RestoreTestReport
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, resultPayload)
|
|
}
|
|
|
|
// handleCancelVerification bedient POST /verification/{id}/cancel.
|
|
func (handler *verificationHandler) handleCancelVerification(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
actingUser, _ := AuthenticatedUserFromContext(request.Context())
|
|
|
|
jobIdentifier, parseError := parseVerificationIdentifier(request)
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, parseError)
|
|
return
|
|
}
|
|
|
|
existingJob, readError := handler.verificationStore.GetJob(request.Context(), jobIdentifier)
|
|
if readError != nil {
|
|
WriteError(responseWriter, request, requestLogger, translateVerificationError(readError))
|
|
return
|
|
}
|
|
|
|
if cancelError := handler.verificationStore.CancelJob(request.Context(), jobIdentifier); cancelError != nil {
|
|
WriteError(responseWriter, request, requestLogger, translateVerificationError(cancelError))
|
|
return
|
|
}
|
|
|
|
handler.recordAudit(request, actingUser, audit.ActionVerificationCancelled, existingJob.BackupID,
|
|
map[string]any{
|
|
"verification_id": jobIdentifier.String(),
|
|
"verification_type": string(existingJob.VerificationType),
|
|
})
|
|
|
|
cancelledJob, _ := handler.verificationStore.GetJob(request.Context(), jobIdentifier)
|
|
|
|
// Die abgebrochene Pruefung sagt nichts ueber das Backup. Das wird gesagt,
|
|
// damit niemand den Abbruch fuer ein Ergebnis haelt.
|
|
WriteSuccess(responseWriter, request, http.StatusOK, map[string]any{
|
|
"verification": buildVerificationResponse(cancelledJob),
|
|
"message": "Die Pruefung wurde abgebrochen. Sie sagt damit nichts ueber den Zustand des Backups; " +
|
|
"die bisherige Einstufung bleibt unveraendert.",
|
|
})
|
|
}
|
|
|
|
// handleGetAssurance bedient GET /backups/{id}/assurance.
|
|
//
|
|
// Die Bewertung wird bei jedem Aufruf neu berechnet, nicht aus der Datenbank
|
|
// gelesen: Sie haengt am Alter der Messungen und veraltet damit von selbst. Ein
|
|
// gespeicherter Wert wuerde mit jedem Tag falscher, ohne dass sich etwas
|
|
// aendert — genau die stille Beschoenigung, die es hier nicht geben darf.
|
|
func (handler *verificationHandler) handleGetAssurance(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
backupIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger,
|
|
NewBadRequestError("Die Backupkennung ist keine gueltige UUID."))
|
|
|
|
return
|
|
}
|
|
|
|
backupFacts, factsError := handler.verificationStore.BackupAssuranceFacts(request.Context(), backupIdentifier)
|
|
if factsError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewNotFoundError("Das Backup wurde nicht gefunden."))
|
|
return
|
|
}
|
|
|
|
classification := verification.Classify(backupFacts)
|
|
assuranceScore := verification.CalculateScore(backupFacts, time.Now())
|
|
|
|
// Die berechnete Bewertung wird fortgeschrieben, damit eine Uebersicht sie
|
|
// anzeigen kann, ohne sie fuer jede Zeile neu zu berechnen. Massgeblich
|
|
// bleibt die Berechnung — der gespeicherte Wert ist nur ihr Abbild.
|
|
if saveError := handler.verificationStore.SaveAssuranceScore(request.Context(),
|
|
backupIdentifier, assuranceScore); saveError != nil {
|
|
requestLogger.Warn("die bewertung konnte nicht gespeichert werden",
|
|
slog.String("backup", backupIdentifier.String()),
|
|
slog.String("grund", saveError.Error()))
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, assuranceResponse{
|
|
BackupID: backupIdentifier,
|
|
Classification: string(classification),
|
|
ClassificationDescription: classification.Describe(),
|
|
Percentage: assuranceScore.Percentage,
|
|
UnknownInputCount: assuranceScore.UnknownInputCount,
|
|
IsTrustworthy: assuranceScore.IsTrustworthy(),
|
|
Summary: assuranceScore.Summary(),
|
|
MissingMeasurements: assuranceScore.MissingMeasurements(),
|
|
Inputs: assuranceScore.Inputs,
|
|
LastVerifiedAt: backupFacts.LastVerifiedAt,
|
|
LastRestoreTestAt: backupFacts.LastRestoreTestAt,
|
|
})
|
|
}
|
|
|
|
// recordAudit schreibt ein Auditereignis.
|
|
func (handler *verificationHandler) recordAudit(request *http.Request, actingUser auth.User, auditAction audit.Action, backupIdentifier uuid.UUID, auditDetails map[string]any) {
|
|
if handler.auditRecorder == nil {
|
|
return
|
|
}
|
|
|
|
correlationID, _ := logging.CorrelationIDFromContext(request.Context())
|
|
|
|
recordError := handler.auditRecorder.Record(request.Context(), audit.Event{
|
|
UserID: &actingUser.ID,
|
|
ActorUsername: actingUser.Username,
|
|
Action: auditAction,
|
|
EntityType: "backup",
|
|
EntityID: &backupIdentifier,
|
|
Result: audit.ResultSuccess,
|
|
IPAddress: clientIPAddress(request),
|
|
UserAgent: request.UserAgent(),
|
|
CorrelationID: correlationID,
|
|
Details: auditDetails,
|
|
})
|
|
|
|
if recordError != nil {
|
|
logging.WithContext(request.Context(), handler.logger).Error(
|
|
"das auditereignis konnte nicht geschrieben werden",
|
|
slog.String("aktion", string(auditAction)),
|
|
slog.String("grund", recordError.Error()))
|
|
}
|
|
}
|
|
|
|
// buildVerificationResponse wandelt einen Auftrag in seine Darstellung.
|
|
func buildVerificationResponse(sourceJob *verification.Job) verificationResponse {
|
|
builtResponse := verificationResponse{
|
|
ID: sourceJob.ID,
|
|
BackupID: sourceJob.BackupID,
|
|
VerificationType: string(sourceJob.VerificationType),
|
|
Status: string(sourceJob.Status),
|
|
Result: string(sourceJob.Result),
|
|
ChunksChecked: sourceJob.ChunksChecked,
|
|
ChunksMissing: sourceJob.ChunksMissing,
|
|
ChunksCorrupted: sourceJob.ChunksCorrupted,
|
|
BytesRead: sourceJob.BytesRead,
|
|
StartedAt: sourceJob.StartedAt,
|
|
CompletedAt: sourceJob.CompletedAt,
|
|
ErrorMessage: sourceJob.ErrorMessage,
|
|
CorrelationID: sourceJob.CorrelationID,
|
|
CreatedAt: sourceJob.CreatedAt,
|
|
}
|
|
|
|
if sourceJob.Report != nil {
|
|
builtResponse.Summary = sourceJob.Report.Summary()
|
|
}
|
|
|
|
if sourceJob.StartedAt != nil && sourceJob.CompletedAt != nil {
|
|
builtResponse.DurationSeconds = sourceJob.CompletedAt.Sub(*sourceJob.StartedAt).Seconds()
|
|
}
|
|
|
|
return builtResponse
|
|
}
|
|
|
|
// parseVerificationType prueft die angeforderte Pruefart.
|
|
func parseVerificationType(requestedType string) (verification.VerificationType, *APIError) {
|
|
switch verification.VerificationType(requestedType) {
|
|
case verification.TypeManifest, verification.TypeChunkPresence,
|
|
verification.TypeChunkIntegrity, verification.TypeChain, verification.TypeRestoreTest:
|
|
return verification.VerificationType(requestedType), nil
|
|
|
|
case "":
|
|
// Ohne Angabe wird die Blockpruefung gewaehlt: Sie ist die schwaechste
|
|
// Pruefung, die ueberhaupt etwas ueber die Daten aussagt. Die
|
|
// Manifestpruefung als Vorgabe waere bequem und wertlos.
|
|
return verification.TypeChunkIntegrity, nil
|
|
|
|
default:
|
|
return "", NewValidationError(
|
|
"Die Pruefart ist unbekannt. Zulaessig sind manifest, chunk_presence, " +
|
|
"chunk_integrity, chain und restore_test.")
|
|
}
|
|
}
|
|
|
|
// parseVerificationIdentifier liest die Auftragskennung aus dem Pfad.
|
|
func parseVerificationIdentifier(request *http.Request) (uuid.UUID, *APIError) {
|
|
jobIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
return uuid.Nil, NewBadRequestError("Die Pruefkennung ist keine gueltige UUID.")
|
|
}
|
|
|
|
return jobIdentifier, nil
|
|
}
|
|
|
|
// translateVerificationError bildet Fehler der Fachschicht auf API-Fehler ab.
|
|
func translateVerificationError(occurredError error) *APIError {
|
|
switch {
|
|
case errors.Is(occurredError, verification.ErrJobNotFound):
|
|
return NewNotFoundError("Der Pruefauftrag wurde nicht gefunden.")
|
|
|
|
case errors.Is(occurredError, verification.ErrBackupBusy):
|
|
// 409 und nicht 500: Der Aufrufer hat nichts falsch gemacht, das Backup
|
|
// wird nur bereits geprueft.
|
|
conflictError := NewValidationError(
|
|
"Dieses Backup wird bereits geprueft. Warten Sie das Ergebnis ab.")
|
|
conflictError.Code = ErrorCodeConflict
|
|
conflictError.StatusCode = http.StatusConflict
|
|
|
|
return conflictError
|
|
|
|
default:
|
|
return NewInternalError(occurredError)
|
|
}
|
|
}
|