Retention Editor added in UI

Time set in Schedular added
This commit is contained in:
Philipp
2026-05-21 09:05:43 +02:00
parent c272b00c81
commit 4af9f32c46
6 changed files with 86 additions and 9 deletions
+11
View File
@@ -24,6 +24,11 @@ export const config = {
RESTIC_REPOSITORY: process.env.RESTIC_REPOSITORY || '',
RESTIC_PASSWORD: process.env.RESTIC_PASSWORD || '',
},
retention: {
keepDaily: Number(process.env.RESTIC_KEEP_DAILY || 7),
keepWeekly: Number(process.env.RESTIC_KEEP_WEEKLY || 0),
keepMonthly: Number(process.env.RESTIC_KEEP_MONTHLY || 0),
},
};
export const editableEnv = [
@@ -32,6 +37,9 @@ export const editableEnv = [
{ key: 'RESTIC_REPOSITORY', label: 'Restic repository', required: true, secret: false },
{ key: 'RESTIC_PASSWORD', label: 'Restic password', required: true, secret: true },
{ key: 'ZFS_POOL_NAME', label: 'ZFS pool name', required: true, secret: false },
{ key: 'RESTIC_KEEP_DAILY', label: 'Keep daily snapshots', required: false, secret: false },
{ key: 'RESTIC_KEEP_WEEKLY', label: 'Keep weekly snapshots', required: false, secret: false },
{ key: 'RESTIC_KEEP_MONTHLY', label: 'Keep monthly snapshots', required: false, secret: false },
{ key: 'PORT', label: 'API port', required: false, secret: false },
{ key: 'API_TOKEN', label: 'API token', required: false, secret: true },
];
@@ -103,4 +111,7 @@ function applyRuntimeEnv(values) {
config.resticEnv.AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || '';
config.resticEnv.RESTIC_REPOSITORY = process.env.RESTIC_REPOSITORY || '';
config.resticEnv.RESTIC_PASSWORD = process.env.RESTIC_PASSWORD || '';
config.retention.keepDaily = Number(process.env.RESTIC_KEEP_DAILY || 7);
config.retention.keepWeekly = Number(process.env.RESTIC_KEEP_WEEKLY || 0);
config.retention.keepMonthly = Number(process.env.RESTIC_KEEP_MONTHLY || 0);
}
+20 -1
View File
@@ -80,7 +80,7 @@ export async function runBackupJob(job) {
setJobStep(job, 'Applying Restic retention policy');
setJobProgress(job, { percent: 96 });
await runRestic(['forget', '--tag', job.vmName, '--keep-daily', '7', '--prune'], {
await runRestic(retentionArgs(job.vmName), {
log: (line) => appendJobLog(job, line),
});
@@ -115,6 +115,25 @@ function formatBytes(value) {
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
}
function retentionArgs(vmName) {
const args = ['forget', '--tag', vmName, '--prune'];
const keepDaily = positiveInteger(config.retention.keepDaily);
const keepWeekly = positiveInteger(config.retention.keepWeekly);
const keepMonthly = positiveInteger(config.retention.keepMonthly);
if (keepDaily) args.push('--keep-daily', String(keepDaily));
if (keepWeekly) args.push('--keep-weekly', String(keepWeekly));
if (keepMonthly) args.push('--keep-monthly', String(keepMonthly));
if (!keepDaily && !keepWeekly && !keepMonthly) args.push('--keep-daily', '7');
return args;
}
function positiveInteger(value) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0;
}
async function cleanupBackup(job, zvol, snapshotName) {
setJobStep(job, 'Running cleanup');
await spawnCommand('zfs', ['set', 'snapdev=hidden', zvol], {
+32 -6
View File
@@ -35,7 +35,7 @@ async function runDueSchedules() {
for (const schedule of schedules) {
if (!schedule.enabled) continue;
if (!schedule.nextRunAt) {
schedule.nextRunAt = nextRunFrom(now, schedule.intervalHours);
schedule.nextRunAt = nextRunFrom(now, schedule.intervalHours, schedule.timeOfDay);
changed = true;
continue;
}
@@ -45,10 +45,10 @@ async function runDueSchedules() {
await startBackupForVm(schedule.vmName);
schedule.lastRunAt = now.toISOString();
schedule.lastError = '';
schedule.nextRunAt = nextRunFrom(now, schedule.intervalHours);
schedule.nextRunAt = nextRunFrom(now, schedule.intervalHours, schedule.timeOfDay);
} catch (error) {
schedule.lastError = String(error.message || error);
schedule.nextRunAt = nextRunFrom(new Date(now.getTime() + 5 * 60 * 1000), schedule.intervalHours);
schedule.nextRunAt = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
}
changed = true;
}
@@ -90,7 +90,8 @@ function normalizeSchedules(values) {
vmName,
enabled: Boolean(value.enabled),
intervalHours,
nextRunAt: value.nextRunAt || nextRunFrom(new Date(), intervalHours),
timeOfDay: normalizeTimeOfDay(value.timeOfDay),
nextRunAt: value.nextRunAt || nextRunFrom(new Date(), intervalHours, normalizeTimeOfDay(value.timeOfDay)),
lastRunAt: value.lastRunAt || null,
lastError: value.lastError || '',
};
@@ -98,6 +99,31 @@ function normalizeSchedules(values) {
.filter(Boolean);
}
function nextRunFrom(date, intervalHours) {
return new Date(date.getTime() + intervalHours * 60 * 60 * 1000).toISOString();
function nextRunFrom(date, intervalHours, timeOfDay) {
const intervalMs = intervalHours * 60 * 60 * 1000;
const candidate = withTimeOfDay(date, timeOfDay);
while (candidate <= date) {
candidate.setTime(candidate.getTime() + intervalMs);
}
return candidate.toISOString();
}
function withTimeOfDay(date, timeOfDay) {
const [hours, minutes] = normalizeTimeOfDay(timeOfDay).split(':').map(Number);
const next = new Date(date);
next.setHours(hours, minutes, 0, 0);
return next;
}
function normalizeTimeOfDay(value) {
const text = String(value || '02:00').trim();
if (/^\d{2}:\d{2}$/.test(text)) {
const [hours, minutes] = text.split(':').map(Number);
if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
return text;
}
}
return '02:00';
}