fix(backups): default ignore-inode for fuse volumes

This commit is contained in:
Nicolas Meienberger 2026-05-02 08:54:14 +02:00
parent 35dd49d7ac
commit d19d827496
No known key found for this signature in database
3 changed files with 96 additions and 1 deletions

View file

@ -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();

View file

@ -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<Volume["type"]>(["rclone", "sftp", "webdav"]);
const IGNORE_INODE_FLAG = "--ignore-inode";
type BackupExecutionRequest = {
scheduleId: number;
@ -33,6 +35,11 @@ const createBackupRunPayload = async ({
}: BackupExecutionRequest & { jobId: string }): Promise<BackupRunPayload> => {
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);

View file

@ -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 ?? [],
});