diff --git a/Dockerfile b/Dockerfile index fe552ee..b920f7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM oven/bun:1.1.23-alpine as base +FROM oven/bun:1.1.25-alpine AS base +LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX" WORKDIR /app # install dependencies into temp directory @@ -13,6 +14,12 @@ RUN mkdir -p /temp/prod COPY package.json bun.lockb /temp/prod/ RUN cd /temp/prod && bun install --frozen-lockfile --production +FROM base AS builder +RUN apk --no-cache add curl gcc +ENV PATH=/root/.cargo/bin:$PATH +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +RUN cargo install resvg + # copy node_modules from temp directory # then copy all (non-ignored) project files into the image # FROM base AS prerelease @@ -40,12 +47,15 @@ RUN apk --no-cache add \ graphicsmagick \ ghostscript \ vips-tools \ + vips-poppler \ + vips-jxl \ libjxl-tools # this might be needed for some latex use cases, will add it if needed. # texmf-dist-fontsextra \ COPY --from=install /temp/prod/node_modules node_modules +COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg # COPY --from=prerelease /app/src/index.tsx /app/src/ # COPY --from=prerelease /app/package.json . COPY . . diff --git a/README.md b/README.md index 0f79e98..f95ae4e 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ A self-hosted online file converter. Supports 831 different formats. Written wit | Converter | Use case | Converts from | Converts to | |------------------------------------------------------------------------------|---------------|---------------|-------------| | [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 | +| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 | | [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 | -| [XeLaTeX](https://tug.org/xetex/) | Documents | 1 | 1 | +| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 | | [Pandoc](https://pandoc.org/) | Documents | 43 | 65 | | [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 | | [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 | @@ -37,12 +38,15 @@ Any missing converter? Open an issue or pull request! services: convertx: image: ghcr.io/c4illin/convertx + container_name: convertx + restart: unless-stopped ports: - "3000:3000" environment: # Defaults are listed below. All are optional. - ACCOUNT_REGISTRATION=false # 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 + - ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally volumes: - convertx:/app/data ``` diff --git a/compose.yaml b/compose.yaml index 5b870f5..ef4cf7c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,5 +8,6 @@ services: environment: - ACCOUNT_REGISTRATION=true - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 + - ALLOW_UNAUTHENTICATED=true ports: - 3000:3000 diff --git a/package.json b/package.json index f06e408..c472147 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@elysiajs/html": "1.0.2", "@elysiajs/jwt": "^1.1.0", "@elysiajs/static": "1.0.3", - "elysia": "^1.1.5" + "elysia": "^1.1.7" }, "module": "src/index.tsx", "type": "module", @@ -26,18 +26,18 @@ "devDependencies": { "@biomejs/biome": "1.8.3", "@eslint/compat": "^1.1.1", - "@eslint/js": "^9.8.0", + "@eslint/js": "^9.9.0", "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@kitajs/ts-html-plugin": "^4.0.2", "@picocss/pico": "^2.0.6", "@total-typescript/ts-reset": "^0.5.1", "@types/bun": "^1.1.6", "@types/eslint": "^9.6.0", - "@types/node": "^22.1.0", - "@typescript-eslint/eslint-plugin": "^8.0.1", - "@typescript-eslint/parser": "^8.0.1", + "@types/node": "^22.5.0", + "@typescript-eslint/eslint-plugin": "^8.2.0", + "@typescript-eslint/parser": "^8.2.0", "cpy-cli": "^5.0.0", - "eslint": "^9.8.0", + "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-eslint-comments": "^3.2.0", @@ -45,11 +45,11 @@ "eslint-plugin-isaacscript": "^3.12.2", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-simple-import-sort": "^12.1.1", - "knip": "^5.27.0", + "knip": "^5.27.3", "npm-run-all2": "^6.2.2", "prettier": "^3.3.3", "typescript": "^5.5.4", - "typescript-eslint": "^8.0.1" + "typescript-eslint": "^8.2.0" }, "trustedDependencies": ["@biomejs/biome"] } diff --git a/src/converters/main.ts b/src/converters/main.ts index 076c03e..6b86fea 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -25,6 +25,11 @@ import { properties as propertiesLibjxl, } from "./libjxl"; +import { + convert as convertresvg, + properties as propertiesresvg, +} from "./resvg"; + import { normalizeFiletype } from "../helpers/normalizeFiletype"; // This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular @@ -59,6 +64,10 @@ const properties: { properties: propertiesLibjxl, converter: convertLibjxl, }, + resvg: { + properties: propertiesresvg, + converter: convertresvg, + }, vips: { properties: propertiesImage, converter: convertImage, diff --git a/src/converters/resvg.ts b/src/converters/resvg.ts new file mode 100644 index 0000000..e971378 --- /dev/null +++ b/src/converters/resvg.ts @@ -0,0 +1,41 @@ +import { exec } from "node:child_process"; + +export const properties = { + from: { + images: ["svg"], + }, + to: { + images: ["png"], + }, +}; + + +export function convert( + filePath: string, + fileType: string, + convertTo: string, + targetPath: string, + // biome-ignore lint/suspicious/noExplicitAny: + options?: any, +): Promise { + return new Promise((resolve, reject) => { + exec( + `resvg "${filePath}" "${targetPath}"`, + (error, stdout, stderr) => { + if (error) { + reject(`error: ${error}`); + } + + if (stdout) { + console.log(`stdout: ${stdout}`); + } + + if (stderr) { + console.error(`stderr: ${stderr}`); + } + + resolve("success"); + }, + ); + }); +} \ No newline at end of file diff --git a/src/converters/vips.ts b/src/converters/vips.ts index 5522d44..09f3d68 100644 --- a/src/converters/vips.ts +++ b/src/converters/vips.ts @@ -113,22 +113,29 @@ export function convert( // .toFormat(convertTo) // .toFile(targetPath); // } + let action = "copy"; + if (fileType === "pdf") { + action = "pdfload"; + } return new Promise((resolve, reject) => { - exec(`vips copy "${filePath}" "${targetPath}"`, (error, stdout, stderr) => { - if (error) { - reject(`error: ${error}`); - } + exec( + `vips ${action} "${filePath}" "${targetPath}"`, + (error, stdout, stderr) => { + if (error) { + reject(`error: ${error}`); + } - if (stdout) { - console.log(`stdout: ${stdout}`); - } + if (stdout) { + console.log(`stdout: ${stdout}`); + } - if (stderr) { - console.error(`stderr: ${stderr}`); - } + if (stderr) { + console.error(`stderr: ${stderr}`); + } - resolve("success"); - }); + resolve("success"); + }, + ); }); } diff --git a/src/helpers/printVersions.ts b/src/helpers/printVersions.ts index a36da86..51f7433 100644 --- a/src/helpers/printVersions.ts +++ b/src/helpers/printVersions.ts @@ -73,6 +73,16 @@ if (process.env.NODE_ENV === "production") { } }); + exec("resvg -V", (error, stdout) => { + if (error) { + console.error("resvg is not installed"); + } + + if (stdout) { + console.log(`resvg v${stdout.split("\n")[0]}`); + } + }); + exec("bun -v", (error, stdout) => { if (error) { console.error("Bun is not installed. wait what"); diff --git a/src/index.tsx b/src/index.tsx index f4b4d8b..a48c854 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,10 +1,10 @@ import cookie from "@elysiajs/cookie"; import { html } from "@elysiajs/html"; -import { jwt } from "@elysiajs/jwt"; +import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt"; import { staticPlugin } from "@elysiajs/static"; import { Database } from "bun:sqlite"; import { Elysia, t } from "elysia"; -import { randomUUID } from "node:crypto"; +import { randomInt, randomUUID } from "node:crypto"; import { rmSync } from "node:fs"; import { mkdir, unlink } from "node:fs/promises"; import { BaseHtml } from "./components/base"; @@ -30,6 +30,8 @@ const ACCOUNT_REGISTRATION = process.env.ACCOUNT_REGISTRATION === "true" || false; const HTTP_ALLOWED = process.env.HTTP_ALLOWED === "true" || false; +const ALLOW_UNAUTHENTICATED = + process.env.ALLOW_UNAUTHENTICATED === "true" || false; // fileNames: fileNames, // filesToConvert: fileNames.length, @@ -403,25 +405,55 @@ const app = new Elysia({ return redirect("/setup", 302); } - if (!auth?.value) { + if (!auth?.value && !ALLOW_UNAUTHENTICATED) { return redirect("/login", 302); } + // validate jwt - const user = await jwt.verify(auth.value); - if (!user) { - return redirect("/login", 302); + let user: ({ id: string } & JWTPayloadSpec) | false = false; + if (auth?.value) { + user = await jwt.verify(auth.value); + + if (user !== false && user.id) { + if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) { + // make sure user exists in db + const existingUser = db + .query("SELECT * FROM users WHERE id = ?") + .as(User) + .get(user.id); + + if (!existingUser) { + if (auth?.value) { + auth.remove(); + } + return redirect("/login", 302); + } + } + } + } else if (ALLOW_UNAUTHENTICATED) { + const newUserId = String(randomInt(2 ** 24, Number.MAX_SAFE_INTEGER)); + const accessToken = await jwt.sign({ + id: newUserId, + }); + + user = { id: newUserId }; + if (!auth) { + return { + message: "No auth cookie, perhaps your browser is blocking cookies.", + }; + } + + // set cookie + auth.set({ + value: accessToken, + httpOnly: true, + secure: !HTTP_ALLOWED, + maxAge: 60 * 60 * 24 * 1, + sameSite: "strict", + }); } - // make sure user exists in db - const existingUser = db - .query("SELECT * FROM users WHERE id = ?") - .as(User) - .get(user.id); - - if (!existingUser) { - if (auth?.value) { - auth.remove(); - } + if (!user) { return redirect("/login", 302); }