package httpapi import ( "fmt" "strconv" "strings" "time" "github.com/google/uuid" "github.com/syncova/syncova/packages/jobs" "github.com/syncova/syncova/packages/scheduler" ) // buildJobFromRequest wandelt einen Anfragerumpf in einen Auftrag. // // Die Prüfung der Werte bleibt der Fachschicht überlassen; hier wird nur // übersetzt. Zwei Prüfstellen mit eigenen Regeln liefen unweigerlich // auseinander. func buildJobFromRequest(jobPayload jobRequest, creatorID uuid.UUID) (*jobs.Job, *APIError) { parsedSchedule, scheduleError := buildScheduleFromRequest(jobPayload.Schedule) if scheduleError != nil { return nil, scheduleError } jobPriority := scheduler.Priority(jobPayload.Priority) if jobPayload.Priority == "" { jobPriority = scheduler.PriorityNormal } maximumConcurrency := jobPayload.MaximumConcurrency if maximumConcurrency < 1 { maximumConcurrency = 1 } jobSources := make([]jobs.JobSource, 0, len(jobPayload.Sources)) for _, requestedSource := range jobPayload.Sources { jobSources = append(jobSources, jobs.JobSource{ SourceType: jobs.SourceType(requestedSource.Type), SourceID: requestedSource.ID, SourceName: requestedSource.Name, AgentID: requestedSource.AgentID, IncludePatterns: requestedSource.IncludePatterns, ExcludePatterns: requestedSource.ExcludePatterns, }) } builtJob := &jobs.Job{ Name: strings.TrimSpace(jobPayload.Name), Description: strings.TrimSpace(jobPayload.Description), Status: jobs.JobStatusActive, Priority: jobPriority, Schedule: parsedSchedule, Sources: jobSources, RepositoryID: jobPayload.RepositoryID, RetentionPolicyID: jobPayload.RetentionPolicyID, DependsOnJobIDs: jobPayload.DependsOnJobIDs, RecoveryPointObjective: time.Duration(jobPayload.RecoveryPointSeconds) * time.Second, RecoveryTimeObjective: time.Duration(jobPayload.RecoveryTimeSeconds) * time.Second, BandwidthLimitBytesPerSecond: jobPayload.BandwidthLimitBytesPerSecond, BackupMode: jobs.BackupMode(jobPayload.BackupMode), FullBackupWeekday: weekdayFromPayload(jobPayload.FullBackupWeekday), MaximumConcurrency: maximumConcurrency, RetryPolicy: scheduler.DefaultRetryPolicy(), CreatedBy: &creatorID, } // Die fachliche Prüfung läuft hier, damit ein fehlerhafter Auftrag mit 422 // beantwortet wird statt mit einem Datenbankfehler als 500. if validationError := builtJob.Validate(); validationError != nil { return nil, NewValidationError(validationError.Error()) } return builtJob, nil } // buildScheduleFromRequest wandelt eine Zeitplanangabe. func buildScheduleFromRequest(scheduleData scheduleRequest) (scheduler.Schedule, *APIError) { builtSchedule := scheduler.Schedule{ ScheduleType: scheduler.ScheduleType(scheduleData.Type), CronExpression: scheduleData.CronExpression, TimeZone: scheduleData.TimeZone, MonthDays: scheduleData.MonthDays, Interval: time.Duration(scheduleData.IntervalSeconds) * time.Second, } if scheduleData.Time != "" { parsedHour, parsedMinute, timeError := parseClockTime(scheduleData.Time) if timeError != nil { return scheduler.Schedule{}, NewValidationError(timeError.Error()) } builtSchedule.Hour = parsedHour builtSchedule.Minute = parsedMinute } for _, weekdayNumber := range scheduleData.Weekdays { if weekdayNumber < 0 || weekdayNumber > 6 { return scheduler.Schedule{}, NewValidationError( fmt.Sprintf("Der Wochentag %d ist unbekannt; zulässig sind 0 (Sonntag) bis 6 (Samstag).", weekdayNumber)) } builtSchedule.Weekdays = append(builtSchedule.Weekdays, time.Weekday(weekdayNumber)) } return builtSchedule, nil } // parseClockTime liest eine Uhrzeit im Format "HH:MM". func parseClockTime(clockText string) (int, int, error) { hourText, minuteText, hasSeparator := strings.Cut(strings.TrimSpace(clockText), ":") if !hasSeparator { return 0, 0, fmt.Errorf("Die Uhrzeit %q muss im Format HH:MM angegeben werden.", clockText) } parsedHour, hourError := strconv.Atoi(hourText) if hourError != nil || parsedHour < 0 || parsedHour > 23 { return 0, 0, fmt.Errorf("Die Stunde in %q liegt nicht zwischen 00 und 23.", clockText) } parsedMinute, minuteError := strconv.Atoi(minuteText) if minuteError != nil || parsedMinute < 0 || parsedMinute > 59 { return 0, 0, fmt.Errorf("Die Minute in %q liegt nicht zwischen 00 und 59.", clockText) } return parsedHour, parsedMinute, nil } // buildJobResponse wandelt einen Auftrag in seine Darstellung nach außen. func buildJobResponse(sourceJob *jobs.Job) jobResponse { sourceResponses := make([]sourceRequest, 0, len(sourceJob.Sources)) for _, jobSource := range sourceJob.Sources { sourceResponses = append(sourceResponses, sourceRequest{ Type: string(jobSource.SourceType), ID: jobSource.SourceID, Name: jobSource.SourceName, AgentID: jobSource.AgentID, IncludePatterns: jobSource.IncludePatterns, ExcludePatterns: jobSource.ExcludePatterns, }) } return jobResponse{ ID: sourceJob.ID, Name: sourceJob.Name, Description: sourceJob.Description, Status: string(sourceJob.Status), Priority: string(sourceJob.Priority), Schedule: buildScheduleResponse(sourceJob.Schedule), ScheduleDescription: sourceJob.Schedule.Describe(), Sources: sourceResponses, RepositoryID: sourceJob.RepositoryID, RetentionPolicyID: sourceJob.RetentionPolicyID, DependsOnJobIDs: sourceJob.DependsOnJobIDs, RecoveryPointSeconds: int64(sourceJob.RecoveryPointObjective.Seconds()), RecoveryTimeSeconds: int64(sourceJob.RecoveryTimeObjective.Seconds()), BandwidthLimitBytesPerSecond: sourceJob.BandwidthLimitBytesPerSecond, BackupMode: string(sourceJob.BackupMode), FullBackupWeekday: weekdayToPayload(sourceJob.FullBackupWeekday), MaximumConcurrency: sourceJob.MaximumConcurrency, NextRunAt: sourceJob.NextRunAt, LastRunAt: sourceJob.LastRunAt, LastOutcome: string(sourceJob.LastOutcome), PausedAt: sourceJob.PausedAt, CreatedAt: sourceJob.CreatedAt, UpdatedAt: sourceJob.UpdatedAt, } } // buildScheduleResponse wandelt einen Zeitplan in seine Darstellung. func buildScheduleResponse(sourceSchedule scheduler.Schedule) scheduleRequest { scheduleData := scheduleRequest{ Type: string(sourceSchedule.ScheduleType), CronExpression: sourceSchedule.CronExpression, TimeZone: sourceSchedule.TimeZone, MonthDays: sourceSchedule.MonthDays, } if sourceSchedule.Interval > 0 { scheduleData.IntervalSeconds = int64(sourceSchedule.Interval.Seconds()) } // Die Uhrzeit erscheint nur bei den Zeitplänen, die eine haben. Bei einem // Intervallplan wäre "00:00" eine Angabe, die es nicht gibt. switch sourceSchedule.ScheduleType { case scheduler.ScheduleTypeDaily, scheduler.ScheduleTypeWeekly, scheduler.ScheduleTypeMonthly: scheduleData.Time = fmt.Sprintf("%02d:%02d", sourceSchedule.Hour, sourceSchedule.Minute) case scheduler.ScheduleTypeHourly: scheduleData.Time = fmt.Sprintf(":%02d", sourceSchedule.Minute) } for _, scheduleWeekday := range sourceSchedule.Weekdays { scheduleData.Weekdays = append(scheduleData.Weekdays, int(scheduleWeekday)) } return scheduleData } // weekdayFromPayload uebersetzt einen Wochentag aus der Anfrage. // // Ein Wert ausserhalb von 0..6 wird verworfen statt gekappt: Ein // stillschweigend auf Sonntag gesetzter Montag waere ein Fehler, den niemand // bemerkt — die Vollsicherung liefe dann am falschen Tag. func weekdayFromPayload(requestedWeekday *int) *time.Weekday { if requestedWeekday == nil { return nil } if *requestedWeekday < 0 || *requestedWeekday > 6 { return nil } convertedWeekday := time.Weekday(*requestedWeekday) return &convertedWeekday } // weekdayToPayload uebersetzt einen Wochentag fuer die Antwort. func weekdayToPayload(storedWeekday *time.Weekday) *int { if storedWeekday == nil { return nil } convertedValue := int(*storedWeekday) return &convertedValue }