diff --git a/src/helpers/buildDownloadUrl.ts b/src/helpers/buildDownloadUrl.ts
new file mode 100644
index 0000000..2de45e0
--- /dev/null
+++ b/src/helpers/buildDownloadUrl.ts
@@ -0,0 +1,3 @@
+export function buildDownloadUrl(webroot: string, outputPath: string, fileName: string): string {
+ return `${webroot}/download/${outputPath}${encodeURIComponent(fileName)}`;
+}
diff --git a/src/pages/results.tsx b/src/pages/results.tsx
index e439959..64dd221 100644
--- a/src/pages/results.tsx
+++ b/src/pages/results.tsx
@@ -3,6 +3,7 @@ import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { Filename, Jobs } from "../db/types";
+import { buildDownloadUrl } from "../helpers/buildDownloadUrl";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { DownloadIcon } from "../icons/download";
import { DeleteIcon } from "../icons/delete";
@@ -107,7 +108,7 @@ function ResultsArticle({
text-accent-500 underline
hover:text-accent-400
`}
- href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
+ href={buildDownloadUrl(WEBROOT, outputPath, file.output_file_name)}
>
@@ -116,7 +117,7 @@ function ResultsArticle({
text-accent-500 underline
hover:text-accent-400
`}
- href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
+ href={buildDownloadUrl(WEBROOT, outputPath, file.output_file_name)}
download={file.output_file_name}
>
diff --git a/tests/pages/results.test.ts b/tests/pages/results.test.ts
new file mode 100644
index 0000000..7673ad9
--- /dev/null
+++ b/tests/pages/results.test.ts
@@ -0,0 +1,12 @@
+import { expect, test } from "bun:test";
+import { buildDownloadUrl } from "../../src/helpers/buildDownloadUrl";
+
+test("encodes reserved characters in download filenames", () => {
+ expect(buildDownloadUrl("", "1/2/", "clip #1?.gif")).toBe("/download/1/2/clip%20%231%3F.gif");
+});
+
+test("preserves output path segments while encoding the filename", () => {
+ expect(buildDownloadUrl("/convertx", "user/job/", "報告 100%.pdf")).toBe(
+ "/convertx/download/user/job/%E5%A0%B1%E5%91%8A%20100%25.pdf",
+ );
+});