fix(backups): preserve relative include/exclude pattern behavior

This commit is contained in:
Nicolas Meienberger 2025-12-19 23:18:56 +01:00
parent 30685c8481
commit 7879d88a54
2 changed files with 33 additions and 29 deletions

View file

@ -21,15 +21,17 @@ afterEach(() => {
});
describe("executeBackup - include / exclude patterns", () => {
test("should correctly build include and exclude patterns by joining with volume path", async () => {
test("should correctly build include and exclude patterns", async () => {
// arrange
const volume = await createTestVolume();
const repository = await createTestRepository();
const volumePath = getVolumePath(volume);
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
includePatterns: ["include1", "subdir/include2"],
excludePatterns: ["exclude1", "subdir/exclude2"],
includePatterns: ["*.zip", "/Photos", "!/Temp", "!*.log"],
excludePatterns: [".DS_Store", "/Config", "!/Important", "!*.tmp"],
excludeIfPresent: [".nobackup"],
});
@ -37,14 +39,12 @@ describe("executeBackup - include / exclude patterns", () => {
await backupsService.executeBackup(schedule.id);
// assert
const volumePath = getVolumePath(volume);
expect(backupMock).toHaveBeenCalledWith(
expect.anything(),
volumePath,
expect.objectContaining({
include: [path.join(volumePath, "include1"), path.join(volumePath, "subdir/include2")],
exclude: [path.join(volumePath, "exclude1"), path.join(volumePath, "subdir/exclude2")],
include: ["*.zip", path.join(volumePath, "Photos"), `!${path.join(volumePath, "Temp")}`, "!*.log"],
exclude: [".DS_Store", path.join(volumePath, "Config"), `!${path.join(volumePath, "Important")}`, "!*.tmp"],
excludeIfPresent: [".nobackup"],
}),
);
@ -88,11 +88,12 @@ describe("executeBackup - include / exclude patterns", () => {
const alreadyJoinedInclude = path.join(volumePath, "already/joined");
const relativeInclude = "relative/include";
const anchoredInclude = "/anchored/include";
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
includePatterns: [alreadyJoinedInclude, relativeInclude],
includePatterns: [alreadyJoinedInclude, relativeInclude, anchoredInclude],
});
// act
@ -103,7 +104,7 @@ describe("executeBackup - include / exclude patterns", () => {
expect.anything(),
volumePath,
expect.objectContaining({
include: [alreadyJoinedInclude, path.join(volumePath, relativeInclude)],
include: [alreadyJoinedInclude, relativeInclude, path.join(volumePath, "anchored/include")],
}),
);
});

View file

@ -33,6 +33,27 @@ const calculateNextRun = (cronExpression: string): number => {
}
};
const processPattern = (pattern: string, volumePath: string): string => {
let isNegated = false;
let p = pattern;
if (p.startsWith("!")) {
isNegated = true;
p = p.slice(1);
}
if (p.startsWith(volumePath)) {
return pattern;
}
if (p.startsWith("/")) {
const processed = path.join(volumePath, p.slice(1));
return isNegated ? `!${processed}` : processed;
}
return pattern;
};
const listSchedules = async () => {
const schedules = await db.query.backupSchedulesTable.findMany({
with: {
@ -259,16 +280,7 @@ const executeBackup = async (scheduleId: number, manual = false) => {
};
if (schedule.excludePatterns && schedule.excludePatterns.length > 0) {
const excludePatterns = [];
for (const pattern of schedule.excludePatterns) {
if (!pattern.startsWith(volumePath)) {
excludePatterns.push(path.join(volumePath, pattern));
} else {
excludePatterns.push(pattern);
}
}
backupOptions.exclude = excludePatterns;
backupOptions.exclude = schedule.excludePatterns.map((p) => processPattern(p, volumePath));
}
if (schedule.excludeIfPresent && schedule.excludeIfPresent.length > 0) {
@ -276,16 +288,7 @@ const executeBackup = async (scheduleId: number, manual = false) => {
}
if (schedule.includePatterns && schedule.includePatterns.length > 0) {
const includePatterns = [];
for (const pattern of schedule.includePatterns) {
if (!pattern.startsWith(volumePath)) {
includePatterns.push(path.join(volumePath, pattern));
} else {
includePatterns.push(pattern);
}
}
backupOptions.include = includePatterns;
backupOptions.include = schedule.includePatterns.map((p) => processPattern(p, volumePath));
}
const releaseBackupLock = await repoMutex.acquireShared(repository.id, `backup:${volume.name}`);