From 9bfbf714476906737da565e8ec527bfc393d0555 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 25 Feb 2026 17:57:55 +0100 Subject: [PATCH] fix(backups): correctly resolve paths when folder name matches volume name When a selected subfolder had the exact same name as its parent volume mount, the `path.relative` check falsely identified the subfolder path as an absolute path already inside the volume. This caused the entire volume root to be backed up instead of the intended subfolder. Closes #250 --- .../__tests__/backups.patterns.test.ts | 24 ++++++++++--------- app/server/modules/backups/backup.helpers.ts | 5 +--- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/server/modules/backups/__tests__/backups.patterns.test.ts b/app/server/modules/backups/__tests__/backups.patterns.test.ts index 2e26a990..9dee49de 100644 --- a/app/server/modules/backups/__tests__/backups.patterns.test.ts +++ b/app/server/modules/backups/__tests__/backups.patterns.test.ts @@ -53,20 +53,23 @@ describe("executeBackup - include / exclude patterns", () => { ); }); - test("should not join with volume path if pattern already starts with it", async () => { + test("should handle the case where a subfolder has the exact same name as the volume name", async () => { // arrange - const volume = await createTestVolume(); + const volumeName = "SyncFolder"; + const volume = await createTestVolume({ + name: volumeName, + type: "directory", + config: { backend: "directory", path: `/${volumeName}` }, + }); const volumePath = getVolumePath(volume); const repository = await createTestRepository(); - const alreadyJoinedInclude = path.join(volumePath, "already/joined"); - const alreadyJoinedExclude = path.join(volumePath, "already/excluded"); + const selectedPath = `/${volumeName}`; // Selection of the folder inside the volume const schedule = await createTestBackupSchedule({ volumeId: volume.id, repositoryId: repository.id, - includePatterns: [alreadyJoinedInclude], - excludePatterns: [alreadyJoinedExclude], + includePatterns: [selectedPath], }); // act @@ -77,8 +80,8 @@ describe("executeBackup - include / exclude patterns", () => { expect.anything(), volumePath, expect.objectContaining({ - include: [alreadyJoinedInclude], - exclude: [alreadyJoinedExclude], + // Should produce /SyncFolder/SyncFolder and not just /SyncFolder + include: [path.join(volumePath, volumeName)], }), ); }); @@ -89,14 +92,13 @@ describe("executeBackup - include / exclude patterns", () => { const volumePath = getVolumePath(volume); const repository = await createTestRepository(); - 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, anchoredInclude], + includePatterns: [relativeInclude, anchoredInclude], }); // act @@ -107,7 +109,7 @@ describe("executeBackup - include / exclude patterns", () => { expect.anything(), volumePath, expect.objectContaining({ - include: [alreadyJoinedInclude, relativeInclude, path.join(volumePath, "anchored/include")], + include: [relativeInclude, path.join(volumePath, "anchored/include")], }), ); }); diff --git a/app/server/modules/backups/backup.helpers.ts b/app/server/modules/backups/backup.helpers.ts index ed3d5ca5..ab5b3d7d 100644 --- a/app/server/modules/backups/backup.helpers.ts +++ b/app/server/modules/backups/backup.helpers.ts @@ -23,10 +23,7 @@ export const processPattern = (pattern: string, volumePath: string) => { const isNegated = pattern.startsWith("!"); const p = isNegated ? pattern.slice(1) : pattern; - const relative = path.relative(volumePath, p); - const isInsideVolume = !relative.startsWith("..") && !path.isAbsolute(relative); - - if (isInsideVolume || !p.startsWith("/")) { + if (!p.startsWith("/")) { return pattern; }