Merge remote-tracking branch 'origin/main' into chore/typescript-7

This commit is contained in:
Emrik Östling
2026-09-26 03:00:43 +02:00
12 changed files with 158 additions and 30 deletions
+11 -3
View File
@@ -45,6 +45,9 @@ RUN bun run build
FROM base AS release
# install additional dependencies
ENV PIPX_HOME=/opt/pipx
ENV PIPX_BIN_DIR=/usr/local/bin
RUN apt-get update && apt-get install -y \
assimp-utils \
calibre \
@@ -53,6 +56,7 @@ RUN apt-get update && apt-get install -y \
dvisvgm \
ffmpeg \
ghostscript \
gosu \
graphicsmagick \
imagemagick-7.q16 \
inkscape \
@@ -84,8 +88,8 @@ RUN apt-get update && apt-get install -y \
&& pipx install "markitdown[all]" \
&& rm -rf /var/lib/apt/lists/*
# Add pipx bin directory to PATH
ENV PATH="/root/.local/bin:${PATH}"
RUN groupadd -g 1000 convertx && \
useradd -u 1000 -g convertx -d /home/convertx -m -s /bin/bash convertx
# Install VTracer binary
RUN ARCH=$(uname -m) && \
@@ -107,8 +111,12 @@ COPY --from=prerelease /app/dist /app/dist
# COPY . .
RUN mkdir data
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 3000/tcp
# used for calibre
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
ENV NODE_ENV=production
ENTRYPOINT [ "bun", "run", "dist/src/index.js" ]
ENTRYPOINT [ "/entrypoint.sh" ]
CMD [ "bun", "run", "dist/src/index.js" ]
+10 -3
View File
@@ -68,6 +68,7 @@ services:
ports:
- "3000:3000"
environment:
- PUID=1000 # user id for file permissions (should match your host user id)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
# - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection
volumes:
@@ -77,20 +78,26 @@ services:
or
```bash
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
docker run -p 3000:3000 -e PUID=1000 -e PGID=1000 -v ./data:/app/data ghcr.io/c4illin/convertx
```
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
Ensure `PUID` and `PGID` match your host user (`id -u` and `id -g`) to avoid permission issues on the mapped `./data` volume. If you get unable to open database file, verify the directory permissions or run `chown -R $USER:$USER path` on the path you choose.
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
### Environment variables
All are optional, JWT_SECRET is recommended to be set.
> [!WARNING]
> Never expose the service to the internet with ALLOW_UNAUTHENTICATED or ACCOUNT_REGISTRATION true. Only allow users you trust to access the service.
| Name | Default | Description |
| ---------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token |
| PUID | 0 (root) | User ID to run the application process as (it first uses root to chown the files) |
| PGID | PUID if unset, root if both unset | Group ID to run the application process as |
| UMASK | 002 (when PUID is set) | File creation mask for processes and created files |
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
+12 -12
View File
@@ -19,15 +19,15 @@
"@tailwindcss/cli": "^4.3.3",
"@tailwindcss/postcss": "^4.3.3",
"@types/bun": "1.3.14",
"@types/node": "^24.13.6",
"@types/node": "^24.19.0",
"@typescript/native": "npm:typescript@^7.0.2",
"eslint": "^10.11.0",
"eslint-plugin-better-tailwindcss": "^4.7.0",
"globals": "^17.12.0",
"knip": "^6.37.0",
"knip": "^6.38.0",
"npm-run-all2": "^9.0.3",
"postcss": "^8.5.28",
"prettier": "^3.9.8",
"prettier": "^3.9.9",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.3.3",
"typescript": "npm:@typescript/typescript6@^6.0.2",
@@ -267,7 +267,7 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@24.13.6", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg=="],
"@types/node": ["@types/node@24.19.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew=="],
"@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
@@ -483,7 +483,7 @@
"keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
"knip": ["knip@6.37.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.7.0", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.150.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.8.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.11", "yaml": "^2.9.1", "zod": "^4.4.3" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-NKCYuoGKJ+o2AuoCuV1mDvfMYnSSWfDXqxwmzxeHyd8fftjNRl8TBUg3fbSrtJjhm7Vj6sc1PX3WZnZhECs9EQ=="],
"knip": ["knip@6.38.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.7.0", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.150.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.8.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.11", "yaml": "^2.9.1", "zod": "^4.4.3" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-umytiMCaZf02MV9SOO1FEDgbDf6h20VZk3ruJE0kh52kkGjYZFRUAXHJWYoLhvhoHGINO/7GXVJmQ2vxTBvl6A=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
@@ -573,7 +573,7 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.9.8", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A=="],
"prettier": ["prettier@3.9.9", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg=="],
"prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
@@ -601,7 +601,7 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
"string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
@@ -651,7 +651,7 @@
"unbash": ["unbash@4.0.11", "", {}, "sha512-FoSOKV7NEofQSkAefMVHam4ZPKYMxjAydxiV72UFEDNV/YofxjGfiZ2A9pZjdL/lRJzTjcu4PABo1JYJX8N5iQ=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
@@ -683,11 +683,11 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="],
@@ -697,7 +697,7 @@
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.10", "", {}, "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q=="],
"bun-types/@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="],
"bun-types/@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="],
"cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+3
View File
@@ -8,6 +8,9 @@ services:
volumes:
- ./data:/app/data
environment: # Defaults are listed below. All are optional.
- PUID=1000 # user ID for file permissions, defaults to 0
- PGID=1000 # group ID for file permissions, defaults to 0
- UMASK=002 # file creation mask, defaults to 002 when PUID is set
- ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
Executable
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -e
if [[ -n "$PUID" && ! "$PUID" =~ ^[0-9]+$ ]] || [[ -n "$PGID" && ! "$PGID" =~ ^[0-9]+$ ]] || [[ -z "$PUID" && -n "$PGID" ]]; then
echo "[entrypoint] ERROR: PUID and PGID must be numeric integers or unset." >&2
exit 1
fi
# If PUID is not set or is 0, run as root (backwards-compatible default)
if [ -z "$PUID" ] || [ "$PUID" -eq 0 ]; then
if [ -n "$UMASK" ]; then
umask "$UMASK"
fi
if [ $# -eq 0 ]; then
set -- bun run dist/src/index.js
fi
exec "$@"
fi
PGID=${PGID:-$PUID}
UMASK=${UMASK:-002}
umask "$UMASK"
if [ "$PGID" -ne "$(id -g convertx)" ]; then
groupmod -o -g "$PGID" convertx
fi
if [ "$PUID" -ne "$(id -u convertx)" ]; then
usermod -o -u "$PUID" convertx
fi
# Ensure home directory exists and is owned by convertx
mkdir -p /home/convertx
if [ "$(stat -c '%u:%g' /home/convertx)" != "$PUID:$PGID" ]; then
chown -R convertx:convertx /home/convertx
fi
# Ensure /app/data exists and fix permissions only if the mount point ownership differs
mkdir -p /app/data
if [ "$(stat -c '%u:%g' /app/data)" != "$PUID:$PGID" ]; then
find /app/data \( ! -user "$PUID" -o ! -group "$PGID" \) -exec chown -h "$PUID:$PGID" '{}' +
fi
export HOME=/home/convertx
if [ $# -eq 0 ]; then
set -- bun run dist/src/index.js
fi
exec gosu convertx "$@"
+2 -1
View File
@@ -18,7 +18,8 @@
"heif-info",
"potrace",
"soffice",
"perl"
"perl",
"markitdown"
],
"tailwind": {
"entry": ["src/main.css"]
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "convertx-frontend",
"version": "0.18.0",
"version": "0.19.0",
"scripts": {
"dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx",
@@ -37,14 +37,14 @@
"@tailwindcss/postcss": "^4.3.3",
"@types/bun": "1.3.14",
"@typescript/native": "npm:typescript@^7.0.2",
"@types/node": "^24.13.6",
"@types/node": "^24.19.0",
"eslint": "^10.11.0",
"eslint-plugin-better-tailwindcss": "^4.7.0",
"globals": "^17.12.0",
"knip": "^6.37.0",
"knip": "^6.38.0",
"npm-run-all2": "^9.0.3",
"postcss": "^8.5.28",
"prettier": "^3.9.8",
"prettier": "^3.9.9",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.3.3",
"typescript": "npm:@typescript/typescript6@^6.0.2",
+5
View File
@@ -461,6 +461,11 @@ export function convert(
}
}
// Increase rasterization quality for PDF input
if (fileType === "pdf") {
inputArgs.push("-density", "300");
}
// Handle EMF files specifically to avoid LibreOffice delegate issues
if (fileType === "emf") {
// Use direct conversion without delegates for EMF files
+26
View File
@@ -1,5 +1,7 @@
import { Cookie } from "elysia";
import db from "../db/db";
import fs from "node:fs";
import path from "node:path";
import { MAX_CONVERT_PROCESS } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
@@ -187,8 +189,32 @@ export async function handleConvert(
mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
.then((r) => {
if (jobId.value) {
const dir = path.dirname(targetPath);
const parsed = path.parse(targetPath);
const outputFiles = fs
.readdirSync(dir)
.filter((f) => {
if (f === parsed.base) {
return true;
}
return (
f.startsWith(`${parsed.name}-`) &&
f.endsWith(parsed.ext) &&
/^\d+$/.test(f.slice(parsed.name.length + 1, -parsed.ext.length))
);
})
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
if (outputFiles.length > 0) {
for (const outputFile of outputFiles) {
query.run(jobId.value, fileName, outputFile, r);
}
} else {
query.run(jobId.value, fileName, newFileName, r);
}
}
resolve(r);
})
.catch((c) => reject(c));
+16 -1
View File
@@ -3,7 +3,22 @@ import { ExecFileFn } from "./types";
export const properties = {
from: {
document: ["pdf", "powerpoint", "excel", "docx", "pptx", "html"],
document: [
"pdf",
"pptx",
"docx",
"xlsx",
"xls",
"msg",
"wav",
"mp3",
"epub",
"html",
"csv",
"json",
"xml",
"zip",
],
},
to: {
document: ["md"],
+11
View File
@@ -197,6 +197,17 @@ if (process.env.NODE_ENV === "production") {
},
);
exec("markitdown -v", (error, stdout) => {
if (error) {
console.error("markitdown is not installed");
return;
}
if (stdout) {
console.log(`${stdout.split("\n")[0]}`);
}
});
exec("bun -v", (error, stdout) => {
if (error) {
console.error("Bun is not installed. wait what");
+1 -1
View File
@@ -101,7 +101,7 @@ test("prints system information and tool versions in production mode", async ()
expect(consoleLogSpy).toHaveBeenCalledWith("ConvertX v1.0.0-test");
expect(consoleLogSpy).toHaveBeenCalledWith("Ubuntu 22.04 LTS");
expect(readFile).toHaveBeenCalledTimes(1);
expect(exec).toHaveBeenCalledTimes(17); // Corresponds exactly to the number of exec calls in the source code
expect(exec).toHaveBeenCalledTimes(18); // Corresponds exactly to the number of exec calls in the source code
});
test("logs error paths when tools are missing", async () => {