Files
ApiCore/ApiCore-Backend/src/backup/backup-settings.service.ts
T
Nick SzaunigandClaude Opus 5.5 ebeb499a18 fix(backup): keep GUI backup paths under BACKUP_ROOT and limit the schedule
backup:configure could point automatic backups at any writable directory
(e.g. a statically served one) and set a cron of every second; an invalid
expression deleted the running job before failing, silently ending
backups.

- A path entered in the UI is resolved under BACKUP_ROOT (default
  ./backups) and refused if it leaves it; env values stay unrestricted.
- The schedule must be a five-field cron that runs at most hourly.
- reload() validates before replacing the running job.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 02:14:18 +02:00

68 lines
2.0 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuditLogService } from '../audit/audit-log.service';
import { BackupSettings } from '../../generated/prisma';
import { UpdateBackupSettingsDto } from './dto/update-backup-settings.dto';
import { assertBackupCron, assertGuiBackupPath } from './backup-schedule-rules';
const SINGLETON_ID = 'singleton';
function emptyToNull(value: string | undefined): string | null {
return value === '' ? null : (value ?? null);
}
@Injectable()
export class BackupSettingsService {
constructor(
private readonly prisma: PrismaService,
private readonly auditLog: AuditLogService,
) {}
async get(): Promise<BackupSettings> {
const existing = await this.prisma.backupSettings.findUnique({
where: { id: SINGLETON_ID },
});
if (existing) {
return existing;
}
return this.prisma.backupSettings.create({ data: { id: SINGLETON_ID } });
}
async update(
dto: UpdateBackupSettingsDto,
actingUserId: string,
): Promise<BackupSettings> {
if (dto.storagePath) {
assertGuiBackupPath(dto.storagePath);
}
if (dto.cronSchedule) {
assertBackupCron(dto.cronSchedule);
}
const before = await this.get();
const settings = await this.prisma.backupSettings.update({
where: { id: SINGLETON_ID },
data: {
storagePath:
dto.storagePath !== undefined
? emptyToNull(dto.storagePath)
: undefined,
cronSchedule:
dto.cronSchedule !== undefined
? emptyToNull(dto.cronSchedule)
: undefined,
retentionCount:
dto.retentionCount !== undefined ? dto.retentionCount : undefined,
},
});
await this.auditLog.record({
entityType: 'BackupSettings',
entityId: SINGLETON_ID,
action: 'UPDATE',
userId: actingUserId,
beforeData: before,
afterData: settings,
});
return settings;
}
}