diff --git a/app/server/modules/backups/__tests__/backups.patterns.test.ts b/app/server/modules/backups/__tests__/backups.patterns.test.ts index df7b3cb2..fafb9e6d 100644 --- a/app/server/modules/backups/__tests__/backups.patterns.test.ts +++ b/app/server/modules/backups/__tests__/backups.patterns.test.ts @@ -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")], }), ); }); diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index b32c317a..98ad981c 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -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}`);