mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-04-18 13:57:52 -04:00
Separate include patters and included path cleanly to avoid path with special characters to be expanded. Closes https://github.com/nicotsx/zerobyte/discussions/680 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added ability to select specific directories and paths for inclusion in backup schedules, separate from pattern-based rules. * **Bug Fixes & Improvements** * Automatically migrates existing backup configurations to work with the new path selection system. * Enhanced backup restoration to properly handle both selected paths and pattern-based inclusions. * **Chores** * Updated database schema to support path selections in backup schedules. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
import { CronExpressionParser } from "cron-parser";
|
|
import path from "node:path";
|
|
import type { BackupSchedule } from "~/server/db/schema";
|
|
import { toMessage } from "~/server/utils/errors";
|
|
import { logger } from "@zerobyte/core/node";
|
|
|
|
export const calculateNextRun = (cronExpression: string) => {
|
|
try {
|
|
const interval = CronExpressionParser.parse(cronExpression, {
|
|
currentDate: new Date(),
|
|
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
});
|
|
return interval.next().getTime();
|
|
} catch (error) {
|
|
logger.error(`Failed to parse cron expression "${cronExpression}": ${toMessage(error)}`);
|
|
const fallback = new Date();
|
|
fallback.setMinutes(fallback.getMinutes() + 1);
|
|
return fallback.getTime();
|
|
}
|
|
};
|
|
|
|
export const processPattern = (pattern: string, volumePath: string, relative = false) => {
|
|
const isNegated = pattern.startsWith("!");
|
|
const p = isNegated ? pattern.slice(1) : pattern;
|
|
|
|
const ensurePatternIsWithinVolume = (candidate: string) => {
|
|
const resolvedVolumePath = path.resolve(volumePath);
|
|
const resolvedCandidatePath = path.resolve(volumePath, candidate);
|
|
const relativePath = path.relative(resolvedVolumePath, resolvedCandidatePath);
|
|
|
|
if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
|
|
throw new Error(`Include pattern escapes volume root: ${pattern}`);
|
|
}
|
|
};
|
|
|
|
if (!p.startsWith("/")) {
|
|
if (!relative) return pattern;
|
|
ensurePatternIsWithinVolume(p);
|
|
const processed = path.join(volumePath, p);
|
|
return isNegated ? `!${processed}` : processed;
|
|
}
|
|
|
|
if (relative) {
|
|
ensurePatternIsWithinVolume(p.slice(1));
|
|
}
|
|
const processed = path.join(volumePath, p.slice(1));
|
|
return isNegated ? `!${processed}` : processed;
|
|
};
|
|
|
|
export const createBackupOptions = (schedule: BackupSchedule, volumePath: string, signal: AbortSignal) => ({
|
|
tags: [schedule.shortId],
|
|
oneFileSystem: schedule.oneFileSystem,
|
|
signal,
|
|
exclude: schedule.excludePatterns ? schedule.excludePatterns.map((p) => processPattern(p, volumePath)) : undefined,
|
|
excludeIfPresent: schedule.excludeIfPresent ?? undefined,
|
|
includePaths: schedule.includePaths
|
|
? schedule.includePaths.map((p) => processPattern(p, volumePath, true))
|
|
: undefined,
|
|
includePatterns: schedule.includePatterns
|
|
? schedule.includePatterns.map((p) => processPattern(p, volumePath, true))
|
|
: undefined,
|
|
customResticParams: schedule.customResticParams ?? undefined,
|
|
});
|