diff --git a/packages/core/src/restic/commands/__tests__/restore.test.ts b/packages/core/src/restic/commands/__tests__/restore.test.ts index 28b86a92..3fc8dea8 100644 --- a/packages/core/src/restic/commands/__tests__/restore.test.ts +++ b/packages/core/src/restic/commands/__tests__/restore.test.ts @@ -143,6 +143,31 @@ describe("restore command", () => { expect(getOptionValues("--include")).toEqual(["backup.20260301-233001.7z"]); }); + test("escapes selected file brackets when restoring from a non-root target", async () => { + const { getRestoreArg, getOptionValues } = setup(); + + await runRestore( + config, + "snapshot-bracket-file", + "/tmp/restore-target", + { + organizationId: "org-1", + include: [ + "/media/A Very English Scandal (2018) [tmdbid-79299]/Season 01/A Very English Scandal (2018) - S01E01 - Folge 1 [1080p H264 UND AAC] chapters.xml", + ], + selectedItemKind: "file", + }, + mockDeps, + ); + + expect(getRestoreArg()).toBe( + "snapshot-bracket-file:/media/A Very English Scandal (2018) [tmdbid-79299]/Season 01", + ); + expect(getOptionValues("--include")).toEqual([ + "A Very English Scandal (2018) - S01E01 - Folge 1 \\[1080p H264 UND AAC\\] chapters.xml", + ]); + }); + test("treats flag-like snapshot IDs as positional restore args", async () => { const { getArgs, getRestoreArg } = setup(); diff --git a/packages/core/src/restic/commands/restore.ts b/packages/core/src/restic/commands/restore.ts index 1379e896..35b48860 100644 --- a/packages/core/src/restic/commands/restore.ts +++ b/packages/core/src/restic/commands/restore.ts @@ -22,6 +22,8 @@ class ResticRestoreCommandError extends Data.TaggedError("ResticRestoreCommandEr message: string; }> {} +const escapeResticIncludePattern = (pattern: string) => pattern.replace(/[\\*?[\]]/g, "\\$&"); + export const restore = ( config: RepositoryConfig, snapshotId: string, @@ -64,7 +66,7 @@ export const restore = ( if (options.include?.length) { if (target === "/") { for (const pattern of options.include) { - args.push("--include", pattern); + args.push("--include", escapeResticIncludePattern(pattern)); } } else { const strippedIncludes = options.include.map((pattern) => @@ -76,7 +78,7 @@ export const restore = ( if (!includesCoverRestoreRoot) { for (const pattern of strippedIncludes) { - args.push("--include", pattern); + args.push("--include", escapeResticIncludePattern(pattern)); } } }