diff --git a/app/server/modules/backups/__tests__/backups.service.execution.test.ts b/app/server/modules/backups/__tests__/backups.service.execution.test.ts index a4d363aa..88e64757 100644 --- a/app/server/modules/backups/__tests__/backups.service.execution.test.ts +++ b/app/server/modules/backups/__tests__/backups.service.execution.test.ts @@ -311,6 +311,94 @@ describe("backup execution - validation failures", () => { ); }); + test("adds ignore-inode by default for FUSE-backed volumes", async () => { + const { runBackupMock } = setup(); + const volume = await createTestVolume({ + type: "sftp", + config: { + backend: "sftp", + host: "storage.example.com", + port: 22, + username: "backup", + privateKey: "key", + path: "/data", + skipHostKeyCheck: false, + }, + }); + const repository = await createTestRepository(); + const schedule = await createTestBackupSchedule({ + volumeId: volume.id, + repositoryId: repository.id, + }); + + await backupsService.executeBackup(schedule.id); + + expect(runBackupMock).toHaveBeenCalledWith( + "local", + expect.objectContaining({ + payload: expect.objectContaining({ + options: expect.objectContaining({ + customResticParams: ["--ignore-inode"], + }), + }), + }), + ); + }); + + test("does not add ignore-inode by default for directory volumes", async () => { + const { runBackupMock } = setup(); + const volume = await createTestVolume(); + const repository = await createTestRepository(); + const schedule = await createTestBackupSchedule({ + volumeId: volume.id, + repositoryId: repository.id, + }); + + await backupsService.executeBackup(schedule.id); + + expect(runBackupMock).toHaveBeenCalledWith( + "local", + expect.objectContaining({ + payload: expect.objectContaining({ + options: expect.objectContaining({ + customResticParams: [], + }), + }), + }), + ); + }); + + test("does not duplicate ignore-inode when already configured", async () => { + const { runBackupMock } = setup(); + const volume = await createTestVolume({ + type: "rclone", + config: { + backend: "rclone", + remote: "remote", + path: "/data", + }, + }); + const repository = await createTestRepository(); + const schedule = await createTestBackupSchedule({ + volumeId: volume.id, + repositoryId: repository.id, + customResticParams: ["--ignore-inode"], + }); + + await backupsService.executeBackup(schedule.id); + + expect(runBackupMock).toHaveBeenCalledWith( + "local", + expect.objectContaining({ + payload: expect.objectContaining({ + options: expect.objectContaining({ + customResticParams: ["--ignore-inode"], + }), + }), + }), + ); + }); + test("should fail backup when the local agent is unavailable", async () => { const { runBackupMock } = setup(); const volume = await createTestVolume(); diff --git a/app/server/modules/backups/backup-executor.ts b/app/server/modules/backups/backup-executor.ts index cc62a6ca..8fb56432 100644 --- a/app/server/modules/backups/backup-executor.ts +++ b/app/server/modules/backups/backup-executor.ts @@ -11,6 +11,8 @@ import { createBackupOptions } from "./backup.helpers"; import { toErrorDetails } from "../../utils/errors"; const LOCAL_AGENT_ID = "local"; +const FUSE_VOLUME_BACKENDS = new Set(["rclone", "sftp", "webdav"]); +const IGNORE_INODE_FLAG = "--ignore-inode"; type BackupExecutionRequest = { scheduleId: number; @@ -33,6 +35,11 @@ const createBackupRunPayload = async ({ }: BackupExecutionRequest & { jobId: string }): Promise => { const sourcePath = getVolumePath(volume); const { signal: _, ...options } = createBackupOptions(schedule, sourcePath); + + if (FUSE_VOLUME_BACKENDS.has(volume.type) && !options.customResticParams.includes(IGNORE_INODE_FLAG)) { + options.customResticParams = [...options.customResticParams, IGNORE_INODE_FLAG]; + } + const repositoryConfig = await decryptRepositoryConfig(repository.config); const encryptedResticPassword = await resticDeps.getOrganizationResticPassword(organizationId); const resticPassword = await resticDeps.resolveSecret(encryptedResticPassword); diff --git a/app/server/modules/backups/backup.helpers.ts b/app/server/modules/backups/backup.helpers.ts index f7f68ac4..8234dc7f 100644 --- a/app/server/modules/backups/backup.helpers.ts +++ b/app/server/modules/backups/backup.helpers.ts @@ -68,5 +68,5 @@ export const createBackupOptions = (schedule: BackupSchedule, volumePath: string includePatterns: schedule.includePatterns ? schedule.includePatterns.map((p) => processPattern(p, volumePath, true)) : undefined, - customResticParams: schedule.customResticParams ?? undefined, + customResticParams: schedule.customResticParams ?? [], });