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]>
68 lines
2.0 KiB
TypeScript
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;
|
|
}
|
|
}
|