From 7157d2398e3dc29de9dff816236fe96e28c5e118 Mon Sep 17 00:00:00 2001 From: casi3 <47931895+casi-3@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:20:35 +0200 Subject: [PATCH 1/4] Apply EXIF orientation when converting images with ImageMagick (#577) --- src/converters/imagemagick.ts | 4 ++++ tests/converters/imagemagick.test.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/converters/imagemagick.ts b/src/converters/imagemagick.ts index 69eb359..e3fc53c 100644 --- a/src/converters/imagemagick.ts +++ b/src/converters/imagemagick.ts @@ -468,6 +468,10 @@ export function convert( outputArgs.push("-background", "white", "-alpha", "remove"); } + // Apply EXIF orientation so photos (e.g. from phones) don't end up sideways + // when converted to formats where the orientation tag is lost or ignored + outputArgs.push("-auto-orient"); + return new Promise((resolve, reject) => { execFile( "magick", diff --git a/tests/converters/imagemagick.test.ts b/tests/converters/imagemagick.test.ts index e7b17aa..f694045 100644 --- a/tests/converters/imagemagick.test.ts +++ b/tests/converters/imagemagick.test.ts @@ -163,3 +163,19 @@ test("convert respects emf as input filetype", async () => { ); expect(loggedMessage).toBe("stdout: Fake stdout"); }); + +test("convert applies EXIF auto-orient", async () => { + const mockExecFile: ExecFileFn = ( + _cmd: string, + _args: string[], + callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, + ) => { + calls.push(_args); + callback(null, "", ""); + }; + + const result = await convert("input.jpg", "jpg", "png", "output.png", undefined, mockExecFile); + + expect(result).toBe("Done"); + expect(calls[0]).toEqual(expect.arrayContaining(["input.jpg", "-auto-orient", "output.png"])); +}); From 15e4046c19b29592dedfb713e30e53702848681d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=8C=E5=AD=90=E6=BD=94?= Date: Tue, 4 Aug 2026 03:21:08 +0800 Subject: [PATCH 2/4] fix(results): encode download filenames (#587) --- src/helpers/buildDownloadUrl.ts | 3 +++ src/pages/results.tsx | 5 +++-- tests/pages/results.test.ts | 12 ++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 src/helpers/buildDownloadUrl.ts create mode 100644 tests/pages/results.test.ts 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", + ); +}); From 3602f12113bc6f72e3c8da1cc009b0faac3e1e3f Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Mon, 3 Aug 2026 22:37:21 +0300 Subject: [PATCH 3/4] fix(dasel): update converter for v3 CLI (#591) --- src/converters/dasel.ts | 14 ++++++++------ src/converters/types.ts | 4 ++-- tests/converters/dasel.test.ts | 26 ++++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/converters/dasel.ts b/src/converters/dasel.ts index 741d325..c895d3e 100644 --- a/src/converters/dasel.ts +++ b/src/converters/dasel.ts @@ -11,6 +11,10 @@ export const properties = { }, }; +export function buildDaselArgs(filePath: string, fileType: string, convertTo: string): string[] { + return ["--var", `data=${fileType}:file:${filePath}`, "--out", convertTo, "$data"]; +} + export async function convert( filePath: string, fileType: string, @@ -19,14 +23,10 @@ export async function convert( options?: unknown, execFile: ExecFileFn = execFileOriginal, // to make it mockable ): Promise { - const args: string[] = []; - - args.push("--file", filePath); - args.push("--read", fileType); - args.push("--write", convertTo); + const args = buildDaselArgs(filePath, fileType, convertTo); return new Promise((resolve, reject) => { - execFile("dasel", args, (error, stdout, stderr) => { + const childProcess = execFile("dasel", args, (error, stdout, stderr) => { if (error) { reject(`error: ${error}`); return; @@ -44,5 +44,7 @@ export async function convert( } }); }); + + childProcess?.stdin?.end(); }); } diff --git a/src/converters/types.ts b/src/converters/types.ts index 1bfe36c..9e5b048 100644 --- a/src/converters/types.ts +++ b/src/converters/types.ts @@ -1,11 +1,11 @@ -import { ExecFileOptions } from "child_process"; +import type { ChildProcess, ExecFileOptions } from "child_process"; export type ExecFileFn = ( cmd: string, args: string[], callback: (err: Error | null, stdout: string, stderr: string) => void, options?: ExecFileOptions, -) => void; +) => ChildProcess | void; export type ConvertFnWithExecFile = ( filePath: string, diff --git a/tests/converters/dasel.test.ts b/tests/converters/dasel.test.ts index b08bed9..9e5a86a 100644 --- a/tests/converters/dasel.test.ts +++ b/tests/converters/dasel.test.ts @@ -1,6 +1,6 @@ import fs from "fs"; import { beforeEach, afterEach, expect, test, describe } from "bun:test"; -import { convert } from "../../src/converters/dasel"; +import { buildDaselArgs, convert } from "../../src/converters/dasel"; import type { ExecFileFn } from "../../src/converters/types"; const originalWriteFile = fs.writeFile; @@ -21,6 +21,16 @@ describe("convert", () => { fs.writeFile = originalWriteFile; }); + test("should build dasel v3 arguments", () => { + expect(buildDaselArgs("input.yaml", "yaml", "json")).toEqual([ + "--var", + "data=yaml:file:input.yaml", + "--out", + "json", + "$data", + ]); + }); + test("should call dasel with correct arguments and write output", async () => { let calledArgs: Parameters = ["", [], () => {}]; mockExecFile = (cmd, args, callback) => { @@ -48,11 +58,23 @@ describe("convert", () => { ); expect(calledArgs[0]).toBe("dasel"); - expect(calledArgs[1]).toEqual(["--file", "input.yaml", "--read", "yaml", "--write", "json"]); + expect(calledArgs[1]).toEqual(["--var", "data=yaml:file:input.yaml", "--out", "json", "$data"]); expect(writeFileCalled).toBe(true); expect(result).toBe("Done"); }); + test("should close dasel stdin so v3 does not wait for input", async () => { + let stdinEnded = false; + mockExecFile = (cmd, args, callback) => { + callback(null, "output-data", ""); + return { stdin: { end: () => (stdinEnded = true) } } as ReturnType; + }; + + await convert("input.yaml", "yaml", "json", "output.json", undefined, mockExecFile); + + expect(stdinEnded).toBe(true); + }); + test("should reject if execFile returns an error", async () => { mockExecFile = (cmd, args, callback) => callback(new Error("fail"), "", ""); await expect( From 5cf7703dd4d85348318ab3c364888816aa4ef8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emrik=20=C3=96stling?= Date: Tue, 4 Aug 2026 22:02:26 +0200 Subject: [PATCH 4/4] fix: better 404 message in logs (#599) --- eslint.config.ts | 1 + src/index.tsx | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/eslint.config.ts b/eslint.config.ts index a94440a..8b493b3 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -20,6 +20,7 @@ export default defineConfig( parser: eslintParserTypeScript, parserOptions: { project: "./tsconfig.eslint.json", + tsconfigRootDir: import.meta.dirname, }, globals: { ...globals.node, diff --git a/src/index.tsx b/src/index.tsx index b48f31a..05713a8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -50,7 +50,11 @@ const app = new Elysia({ .use(listConverters) .use(chooseConverter) .use(healthcheck) - .onError(({ error }) => { + .onError(({ error, code, request }) => { + if (code === "NOT_FOUND") { + console.warn(`404: ${request.method} ${new URL(request.url).pathname}`); + return; + } console.error(error); });