Merge remote-tracking branch 'upstream/main' into feat/oidc-sso-login
This commit is contained in:
commit
5f1d3bca10
|
|
@ -20,6 +20,7 @@ export default defineConfig(
|
|||
parser: eslintParserTypeScript,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.eslint.json",
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
globals: {
|
||||
...globals.node,
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export function buildDownloadUrl(webroot: string, outputPath: string, fileName: string): string {
|
||||
return `${webroot}/download/${outputPath}${encodeURIComponent(fileName)}`;
|
||||
}
|
||||
|
|
@ -52,7 +52,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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
>
|
||||
<EyeIcon />
|
||||
</a>
|
||||
|
|
@ -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}
|
||||
>
|
||||
<DownloadIcon />
|
||||
|
|
|
|||
|
|
@ -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<ExecFileFn> = ["", [], () => {}];
|
||||
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<ExecFileFn>;
|
||||
};
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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"]));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue