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
This commit is contained in:
Nicolas Meienberger 2026-02-25 17:57:55 +01:00 committed by Nico
parent cc55fee339
commit 9bfbf71447
2 changed files with 14 additions and 15 deletions

View file

@ -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 // 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 volumePath = getVolumePath(volume);
const repository = await createTestRepository(); const repository = await createTestRepository();
const alreadyJoinedInclude = path.join(volumePath, "already/joined"); const selectedPath = `/${volumeName}`; // Selection of the folder inside the volume
const alreadyJoinedExclude = path.join(volumePath, "already/excluded");
const schedule = await createTestBackupSchedule({ const schedule = await createTestBackupSchedule({
volumeId: volume.id, volumeId: volume.id,
repositoryId: repository.id, repositoryId: repository.id,
includePatterns: [alreadyJoinedInclude], includePatterns: [selectedPath],
excludePatterns: [alreadyJoinedExclude],
}); });
// act // act
@ -77,8 +80,8 @@ describe("executeBackup - include / exclude patterns", () => {
expect.anything(), expect.anything(),
volumePath, volumePath,
expect.objectContaining({ expect.objectContaining({
include: [alreadyJoinedInclude], // Should produce /SyncFolder/SyncFolder and not just /SyncFolder
exclude: [alreadyJoinedExclude], include: [path.join(volumePath, volumeName)],
}), }),
); );
}); });
@ -89,14 +92,13 @@ describe("executeBackup - include / exclude patterns", () => {
const volumePath = getVolumePath(volume); const volumePath = getVolumePath(volume);
const repository = await createTestRepository(); const repository = await createTestRepository();
const alreadyJoinedInclude = path.join(volumePath, "already/joined");
const relativeInclude = "relative/include"; const relativeInclude = "relative/include";
const anchoredInclude = "/anchored/include"; const anchoredInclude = "/anchored/include";
const schedule = await createTestBackupSchedule({ const schedule = await createTestBackupSchedule({
volumeId: volume.id, volumeId: volume.id,
repositoryId: repository.id, repositoryId: repository.id,
includePatterns: [alreadyJoinedInclude, relativeInclude, anchoredInclude], includePatterns: [relativeInclude, anchoredInclude],
}); });
// act // act
@ -107,7 +109,7 @@ describe("executeBackup - include / exclude patterns", () => {
expect.anything(), expect.anything(),
volumePath, volumePath,
expect.objectContaining({ expect.objectContaining({
include: [alreadyJoinedInclude, relativeInclude, path.join(volumePath, "anchored/include")], include: [relativeInclude, path.join(volumePath, "anchored/include")],
}), }),
); );
}); });

View file

@ -23,10 +23,7 @@ export const processPattern = (pattern: string, volumePath: string) => {
const isNegated = pattern.startsWith("!"); const isNegated = pattern.startsWith("!");
const p = isNegated ? pattern.slice(1) : pattern; const p = isNegated ? pattern.slice(1) : pattern;
const relative = path.relative(volumePath, p); if (!p.startsWith("/")) {
const isInsideVolume = !relative.startsWith("..") && !path.isAbsolute(relative);
if (isInsideVolume || !p.startsWith("/")) {
return pattern; return pattern;
} }