Merge pull request #413 from C4illin/devcontainer

This commit is contained in:
Emrik Östling
2025-10-05 17:14:42 +02:00
committed by GitHub
15 changed files with 1225 additions and 1097 deletions

69
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,69 @@
FROM debian:trixie-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
curl \
unzip \
git \
ca-certificates \
build-essential \
assimp-utils \
calibre \
dasel \
dcraw \
dvisvgm \
ffmpeg \
ghostscript \
graphicsmagick \
imagemagick-7.q16 \
inkscape \
latexmk \
libheif-examples \
libjxl-tools \
libreoffice \
libva2 \
libvips-tools \
libemail-outlook-message-perl \
lmodern \
mupdf-tools \
pandoc \
poppler-utils \
potrace \
python3-numpy \
resvg \
texlive \
texlive-fonts-recommended \
texlive-latex-extra \
texlive-latex-recommended \
texlive-xetex \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \
curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
else \
curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \
fi && \
unzip -j bun-linux-*.zip -d /usr/local/bin && \
rm bun-linux-*.zip && \
chmod +x /usr/local/bin/bun
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \
VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \
else \
VTRACER_ASSET="vtracer-x86_64-unknown-linux-musl.tar.gz"; \
fi && \
curl -L -o /tmp/vtracer.tar.gz "https://github.com/visioncortex/vtracer/releases/download/0.6.4/${VTRACER_ASSET}" && \
tar -xzf /tmp/vtracer.tar.gz -C /tmp/ && \
mv /tmp/vtracer /usr/local/bin/vtracer && \
chmod +x /usr/local/bin/vtracer && \
rm /tmp/vtracer.tar.gz
RUN mkdir -p data
ENV NODE_ENV=development
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
EXPOSE 3000
CMD ["bun", "run", "dev"]

View File

@@ -0,0 +1,50 @@
{
"name": "ConvertX Development Environment",
"build": {
"dockerfile": "Dockerfile"
},
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.vscode-typescript-next",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-json",
"ms-vscode.vscode-docker",
"oven.bun-vscode"
],
"settings": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.suggest.autoImports": true,
"tailwindCSS.includeLanguages": {
"typescript": "javascript",
"typescriptreact": "javascript"
},
"files.associations": {
"*.css": "tailwindcss"
},
"terminal.integrated.defaultProfile.linux": "bash"
}
}
},
"forwardPorts": [3000],
"portsAttributes": {
"3000": {
"label": "ConvertX Application",
"onAutoForward": "notify"
}
},
"postCreateCommand": "bun install",
"remoteUser": "root",
"mounts": ["source=${localWorkspaceFolder}/data,target=/app/data,type=bind"]
}

104
.gitignore vendored
View File

@@ -1,52 +1,52 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
/node_modules /node_modules
/.pnp /.pnp
.pnp.js .pnp.js
# testing # testing
/coverage /coverage
# next.js # next.js
/.next/ /.next/
/out/ /out/
# production # production
/build /build
# misc # misc
.DS_Store .DS_Store
*.pem *.pem
# debug # debug
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files # local env files
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
# vercel # vercel
.vercel .vercel
**/*.trace **/*.trace
**/*.zip **/*.zip
**/*.tar.gz **/*.tar.gz
**/*.tgz **/*.tgz
**/*.log **/*.log
package-lock.json package-lock.json
**/*.bun **/*.bun
/src/uploads /src/uploads
/uploads /uploads
/mydb.sqlite /mydb.sqlite
/output /output
/db /db
/data /data
/dist /dist
/Bruno /Bruno
/tsconfig.tsbuildinfo /tsconfig.tsbuildinfo
/public/generated.css /public/generated.css

View File

@@ -1,104 +1,104 @@
FROM debian:trixie-slim AS base FROM debian:trixie-slim AS base
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX" LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
WORKDIR /app WORKDIR /app
# install bun # install bun
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
curl \ curl \
unzip \ unzip \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# if architecture is arm64, use the arm64 version of bun # if architecture is arm64, use the arm64 version of bun
RUN ARCH=$(uname -m) && \ RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \ if [ "$ARCH" = "aarch64" ]; then \
curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \ curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
else \ else \
curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \ curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \
fi fi
RUN unzip -j bun-linux-*.zip -d /usr/local/bin && \ RUN unzip -j bun-linux-*.zip -d /usr/local/bin && \
rm bun-linux-*.zip && \ rm bun-linux-*.zip && \
chmod +x /usr/local/bin/bun chmod +x /usr/local/bin/bun
# install dependencies into temp directory # install dependencies into temp directory
# this will cache them and speed up future builds # this will cache them and speed up future builds
FROM base AS install FROM base AS install
RUN mkdir -p /temp/dev RUN mkdir -p /temp/dev
COPY package.json bun.lock /temp/dev/ COPY package.json bun.lock /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies) # install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/ COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production RUN cd /temp/prod && bun install --frozen-lockfile --production
FROM base AS prerelease FROM base AS prerelease
WORKDIR /app WORKDIR /app
COPY --from=install /temp/dev/node_modules node_modules COPY --from=install /temp/dev/node_modules node_modules
COPY . . COPY . .
# ENV NODE_ENV=production # ENV NODE_ENV=production
RUN bun run build RUN bun run build
# copy production dependencies and source code into final image # copy production dependencies and source code into final image
FROM base AS release FROM base AS release
# install additional dependencies # install additional dependencies
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
assimp-utils \ assimp-utils \
calibre \ calibre \
dasel \ dasel \
dcraw \ dcraw \
dvisvgm \ dvisvgm \
ffmpeg \ ffmpeg \
ghostscript \ ghostscript \
graphicsmagick \ graphicsmagick \
imagemagick-7.q16 \ imagemagick-7.q16 \
inkscape \ inkscape \
latexmk \ latexmk \
libheif-examples \ libheif-examples \
libjxl-tools \ libjxl-tools \
libreoffice \ libreoffice \
libva2 \ libva2 \
libvips-tools \ libvips-tools \
libemail-outlook-message-perl \ libemail-outlook-message-perl \
lmodern \ lmodern \
mupdf-tools \ mupdf-tools \
pandoc \ pandoc \
poppler-utils \ poppler-utils \
potrace \ potrace \
python3-numpy \ python3-numpy \
resvg \ resvg \
texlive \ texlive \
texlive-fonts-recommended \ texlive-fonts-recommended \
texlive-latex-extra \ texlive-latex-extra \
texlive-latex-recommended \ texlive-latex-recommended \
texlive-xetex \ texlive-xetex \
--no-install-recommends \ --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install VTracer binary # Install VTracer binary
RUN ARCH=$(uname -m) && \ RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \ if [ "$ARCH" = "aarch64" ]; then \
VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \ VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \
else \ else \
VTRACER_ASSET="vtracer-x86_64-unknown-linux-musl.tar.gz"; \ VTRACER_ASSET="vtracer-x86_64-unknown-linux-musl.tar.gz"; \
fi && \ fi && \
curl -L -o /tmp/vtracer.tar.gz "https://github.com/visioncortex/vtracer/releases/download/0.6.4/${VTRACER_ASSET}" && \ curl -L -o /tmp/vtracer.tar.gz "https://github.com/visioncortex/vtracer/releases/download/0.6.4/${VTRACER_ASSET}" && \
tar -xzf /tmp/vtracer.tar.gz -C /tmp/ && \ tar -xzf /tmp/vtracer.tar.gz -C /tmp/ && \
mv /tmp/vtracer /usr/local/bin/vtracer && \ mv /tmp/vtracer /usr/local/bin/vtracer && \
chmod +x /usr/local/bin/vtracer && \ chmod +x /usr/local/bin/vtracer && \
rm /tmp/vtracer.tar.gz rm /tmp/vtracer.tar.gz
COPY --from=install /temp/prod/node_modules node_modules COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /app/public/ /app/public/ COPY --from=prerelease /app/public/ /app/public/
COPY --from=prerelease /app/dist /app/dist COPY --from=prerelease /app/dist /app/dist
# COPY . . # COPY . .
RUN mkdir data RUN mkdir data
EXPOSE 3000/tcp EXPOSE 3000/tcp
# used for calibre # used for calibre
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox" ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
ENV NODE_ENV=production ENV NODE_ENV=production
ENTRYPOINT [ "bun", "run", "dist/src/index.js" ] ENTRYPOINT [ "bun", "run", "dist/src/index.js" ]

1322
LICENSE

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,6 @@
"@total-typescript/ts-reset": "^0.6.1", "@total-typescript/ts-reset": "^0.6.1",
"@types/bun": "latest", "@types/bun": "latest",
"@types/node": "^24.6.2", "@types/node": "^24.6.2",
"@types/tar": "^6.1.13",
"@typescript-eslint/parser": "^8.45.0", "@typescript-eslint/parser": "^8.45.0",
"eslint": "^9.37.0", "eslint": "^9.37.0",
"eslint-plugin-better-tailwindcss": "^3.7.9", "eslint-plugin-better-tailwindcss": "^3.7.9",
@@ -240,8 +239,6 @@
"@types/react": ["@types/react@19.1.15", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA=="], "@types/react": ["@types/react@19.1.15", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA=="],
"@types/tar": ["@types/tar@6.1.13", "", { "dependencies": { "@types/node": "*", "minipass": "^4.0.0" } }, "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.45.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.45.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ=="],
@@ -470,7 +467,7 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
@@ -636,8 +633,6 @@
"@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"@isaacs/fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], "@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
"@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], "@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
@@ -658,8 +653,6 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/tar/@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -674,10 +667,6 @@
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"minizlib/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"tar/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"tsconfig-paths-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "tsconfig-paths-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], "@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
@@ -704,8 +693,6 @@
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/tar/@types/node/undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],

View File

@@ -1,2 +1,2 @@
[tools] [tools]
bun = "1.2.2" bun = "1.2.2"

View File

@@ -4,12 +4,12 @@
"scripts": { "scripts": {
"dev": "bun run --watch src/index.tsx", "dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx", "hot": "bun run --hot src/index.tsx",
"format": "run-p 'format:*'", "format": "npm-run-all 'format:*'",
"format:eslint": "eslint --fix .", "format:eslint": "eslint --fix .",
"format:prettier": "prettier --write .", "format:prettier": "prettier --write .",
"build:js": "tsc", "build:js": "tsc",
"build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css && bun run build:js", "build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css && bun run build:js",
"lint": "run-p 'lint:*'", "lint": "npm-run-all 'lint:*'",
"lint:tsc": "tsc --noEmit", "lint:tsc": "tsc --noEmit",
"lint:knip": "knip", "lint:knip": "knip",
"lint:eslint": "eslint .", "lint:eslint": "eslint .",
@@ -38,7 +38,6 @@
"@total-typescript/ts-reset": "^0.6.1", "@total-typescript/ts-reset": "^0.6.1",
"@types/bun": "latest", "@types/bun": "latest",
"@types/node": "^24.6.2", "@types/node": "^24.6.2",
"@types/tar": "^6.1.13",
"@typescript-eslint/parser": "^8.45.0", "@typescript-eslint/parser": "^8.45.0",
"eslint": "^9.37.0", "eslint": "^9.37.0",
"eslint-plugin-better-tailwindcss": "^3.7.9", "eslint-plugin-better-tailwindcss": "^3.7.9",

View File

@@ -1,2 +1,2 @@
User-agent: * User-agent: *
Disallow: / Disallow: /

View File

@@ -1,5 +1,5 @@
import path from "node:path"; import path from "node:path";
import { Elysia, t } from 'elysia' import { Elysia } from "elysia";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import * as tar from "tar"; import * as tar from "tar";
import { outputDir } from ".."; import { outputDir } from "..";
@@ -29,33 +29,37 @@ export const download = new Elysia()
}, },
{ {
auth: true, auth: true,
} },
) )
.get("/archive/:userId/:jobId", async ({ params, redirect, user }) => { .get(
const job = await db "/archive/:userId/:jobId",
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") async ({ params, redirect, user }) => {
.get(user.id, params.jobId); const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);
if (!job) { if (!job) {
return redirect(`${WEBROOT}/results`, 302); return redirect(`${WEBROOT}/results`, 302);
} }
const userId = decodeURIComponent(params.userId); const userId = decodeURIComponent(params.userId);
const jobId = decodeURIComponent(params.jobId); const jobId = decodeURIComponent(params.jobId);
const outputPath = `${outputDir}${userId}/${jobId}`; const outputPath = `${outputDir}${userId}/${jobId}`;
const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`); const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
await tar.create( await tar.create(
{ {
file: outputTar, file: outputTar,
cwd: outputPath, cwd: outputPath,
filter: (path) => { filter: (path) => {
return !path.match(".*\\.tar"); return !path.match(".*\\.tar");
},
}, },
}, ["."],
["."], );
); return Bun.file(outputTar);
return Bun.file(outputTar); },
}, { {
auth: true, auth: true,
}); },
);

View File

@@ -7,9 +7,9 @@ import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env"; import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
export const history = new Elysia() export const history = new Elysia().use(userService).get(
.use(userService) "/history",
.get("/history", async ({ jwt, redirect, user }) => { async ({ redirect, user }) => {
if (HIDE_HISTORY) { if (HIDE_HISTORY) {
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} }
@@ -208,6 +208,8 @@ export const history = new Elysia()
</> </>
</BaseHtml> </BaseHtml>
); );
}, { },
auth: true {
}); auth: true,
},
);

View File

@@ -6,9 +6,9 @@ import { getAllInputs, getAllTargets } from "../converters/main";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
export const listConverters = new Elysia() export const listConverters = new Elysia().use(userService).get(
.use(userService) "/converters",
.get("/converters", async () => { async () => {
return ( return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters"> <BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
<> <>
@@ -68,6 +68,8 @@ export const listConverters = new Elysia()
</> </>
</BaseHtml> </BaseHtml>
); );
}, { },
auth: true {
}); auth: true,
},
);

View File

@@ -136,72 +136,80 @@ function ResultsArticle({
export const results = new Elysia() export const results = new Elysia()
.use(userService) .use(userService)
.get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { job_id }, user }) => { .get(
if (job_id?.value) { "/results/:jobId",
// Clear the job_id cookie since we are viewing the results async ({ params, set, cookie: { job_id }, user }) => {
job_id.remove(); if (job_id?.value) {
} // Clear the job_id cookie since we are viewing the results
job_id.remove();
}
const job = db const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) .as(Jobs)
.get(user.id, params.jobId); .get(user.id, params.jobId);
if (!job) { if (!job) {
set.status = 404; set.status = 404;
return { return {
message: "Job not found.", message: "Job not found.",
}; };
} }
const outputPath = `${user.id}/${params.jobId}/`; const outputPath = `${user.id}/${params.jobId}/`;
const files = db const files = db
.query("SELECT * FROM file_names WHERE job_id = ?") .query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename) .as(Filename)
.all(params.jobId); .all(params.jobId);
return ( return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Result"> <BaseHtml webroot={WEBROOT} title="ConvertX | Result">
<> <>
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn /> <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
<main <main
class={` class={`
w-full flex-1 px-2 w-full flex-1 px-2
sm:px-4 sm:px-4
`} `}
> >
<ResultsArticle user={user} job={job} files={files} outputPath={outputPath} /> <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />
</main> </main>
<script src={`${WEBROOT}/results.js`} defer /> <script src={`${WEBROOT}/results.js`} defer />
</> </>
</BaseHtml> </BaseHtml>
); );
}, { auth: true }) },
.post("/progress/:jobId", async ({ jwt, set, params, cookie: { job_id }, user }) => { { auth: true },
if (job_id?.value) { )
// Clear the job_id cookie since we are viewing the results .post(
job_id.remove(); "/progress/:jobId",
} async ({ set, params, cookie: { job_id }, user }) => {
if (job_id?.value) {
// Clear the job_id cookie since we are viewing the results
job_id.remove();
}
const job = db const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) .as(Jobs)
.get(user.id, params.jobId); .get(user.id, params.jobId);
if (!job) { if (!job) {
set.status = 404; set.status = 404;
return { return {
message: "Job not found.", message: "Job not found.",
}; };
} }
const outputPath = `${user.id}/${params.jobId}/`; const outputPath = `${user.id}/${params.jobId}/`;
const files = db const files = db
.query("SELECT * FROM file_names WHERE job_id = ?") .query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename) .as(Filename)
.all(params.jobId); .all(params.jobId);
return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />; return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
}, { auth: true }); },
{ auth: true },
);

View File

@@ -17,9 +17,9 @@ import {
} from "../helpers/env"; } from "../helpers/env";
import { FIRST_RUN, userService } from "./user"; import { FIRST_RUN, userService } from "./user";
export const root = new Elysia() export const root = new Elysia().use(userService).get(
.use(userService) "/",
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { async ({ jwt, redirect, cookie: { auth, jobId } }) => {
if (!ALLOW_UNAUTHENTICATED) { if (!ALLOW_UNAUTHENTICATED) {
if (FIRST_RUN) { if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302); return redirect(`${WEBROOT}/setup`, 302);
@@ -240,9 +240,11 @@ export const root = new Elysia()
</> </>
</BaseHtml> </BaseHtml>
); );
}, { },
{
cookie: t.Cookie({ cookie: t.Cookie({
auth: t.Optional(t.String()), auth: t.Optional(t.String()),
jobId: t.Optional(t.String()), jobId: t.Optional(t.String()),
}) }),
}); },
);

View File

@@ -39,30 +39,29 @@ export const userService = new Elysia({ name: "user/service" })
optionalSession: t.Cookie({ optionalSession: t.Cookie({
auth: t.Optional(t.String()), auth: t.Optional(t.String()),
jobId: t.Optional(t.String()), jobId: t.Optional(t.String()),
}) }),
}) })
.macro("auth", { .macro("auth", {
cookie: "session", async resolve({ cookie: "session",
status, jwt, cookie: { auth } async resolve({ status, jwt, cookie: { auth } }) {
}) {
if (!auth.value) { if (!auth.value) {
return status(401, { return status(401, {
success: false, success: false,
message: 'Unauthorized' message: "Unauthorized",
}) });
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return status(401, { return status(401, {
success: false, success: false,
message: 'Unauthorized' message: "Unauthorized",
}) });
} }
return { return {
success: true, success: true,
user user,
}; };
} },
}); });
export const user = new Elysia() export const user = new Elysia()
@@ -237,82 +236,85 @@ export const user = new Elysia()
}, },
{ body: "signIn" }, { body: "signIn" },
) )
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => { .get(
if (FIRST_RUN) { "/login",
return redirect(`${WEBROOT}/setup`, 302); async ({ jwt, redirect, cookie: { auth } }) => {
} if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302);
// if already logged in, redirect to home
if (auth?.value) {
const user = await jwt.verify(auth.value);
if (user) {
return redirect(`${WEBROOT}/`, 302);
} }
auth.remove(); // if already logged in, redirect to home
} if (auth?.value) {
const user = await jwt.verify(auth.value);
return ( if (user) {
<BaseHtml webroot={WEBROOT} title="ConvertX | Login"> return redirect(`${WEBROOT}/`, 302);
<> }
<Header
webroot={WEBROOT} auth.remove();
accountRegistration={ACCOUNT_REGISTRATION} }
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY} return (
/> <BaseHtml webroot={WEBROOT} title="ConvertX | Login">
<main <>
class={` <Header
w-full flex-1 px-2 webroot={WEBROOT}
sm:px-4 accountRegistration={ACCOUNT_REGISTRATION}
`} allowUnauthenticated={ALLOW_UNAUTHENTICATED}
> hideHistory={HIDE_HISTORY}
<article class="article"> />
<form method="post" class="flex flex-col gap-4"> <main
<fieldset class="mb-4 flex flex-col gap-4"> class={`
<label class="flex flex-col gap-1"> w-full flex-1 px-2
Email sm:px-4
<input `}
type="email" >
name="email" <article class="article">
class="rounded-sm bg-neutral-800 p-3" <form method="post" class="flex flex-col gap-4">
placeholder="Email" <fieldset class="mb-4 flex flex-col gap-4">
autocomplete="email" <label class="flex flex-col gap-1">
required Email
/> <input
</label> type="email"
<label class="flex flex-col gap-1"> name="email"
Password class="rounded-sm bg-neutral-800 p-3"
<input placeholder="Email"
type="password" autocomplete="email"
name="password" required
class="rounded-sm bg-neutral-800 p-3" />
placeholder="Password" </label>
autocomplete="current-password" <label class="flex flex-col gap-1">
required Password
/> <input
</label> type="password"
</fieldset> name="password"
<div class="flex flex-row gap-4"> class="rounded-sm bg-neutral-800 p-3"
{ACCOUNT_REGISTRATION ? ( placeholder="Password"
<a autocomplete="current-password"
href={`${WEBROOT}/register`} required
role="button" />
class="w-full btn-secondary text-center" </label>
> </fieldset>
Register <div class="flex flex-row gap-4">
</a> {ACCOUNT_REGISTRATION ? (
) : null} <a
<input type="submit" value="Login" class="w-full btn-primary" /> href={`${WEBROOT}/register`}
</div> role="button"
</form> class="w-full btn-secondary text-center"
</article> >
</main> Register
</> </a>
</BaseHtml> ) : null}
); <input type="submit" value="Login" class="w-full btn-primary" />
}, { body: "signIn", cookie: "optionalSession" } </div>
</form>
</article>
</main>
</>
</BaseHtml>
);
},
{ body: "signIn", cookie: "optionalSession" },
) )
.post( .post(
"/login", "/login",
@@ -373,83 +375,86 @@ export const user = new Elysia()
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
}) })
.get("/account", async ({ user, redirect }) => { .get(
"/account",
async ({ user, redirect }) => {
if (!user) {
return redirect(`${WEBROOT}/`, 302);
}
if (!user) { const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
return redirect(`${WEBROOT}/`, 302);
}
const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id); if (!userData) {
return redirect(`${WEBROOT}/`, 302);
}
if (!userData) { return (
return redirect(`${WEBROOT}/`, 302); <BaseHtml webroot={WEBROOT} title="ConvertX | Account">
} <>
<Header
return ( webroot={WEBROOT}
<BaseHtml webroot={WEBROOT} title="ConvertX | Account"> accountRegistration={ACCOUNT_REGISTRATION}
<> allowUnauthenticated={ALLOW_UNAUTHENTICATED}
<Header hideHistory={HIDE_HISTORY}
webroot={WEBROOT} loggedIn
accountRegistration={ACCOUNT_REGISTRATION} />
allowUnauthenticated={ALLOW_UNAUTHENTICATED} <main
hideHistory={HIDE_HISTORY} class={`
loggedIn w-full flex-1 px-2
/> sm:px-4
<main `}
class={` >
w-full flex-1 px-2 <article class="article">
sm:px-4 <form method="post" class="flex flex-col gap-4">
`} <fieldset class="mb-4 flex flex-col gap-4">
> <label class="flex flex-col gap-1">
<article class="article"> Email
<form method="post" class="flex flex-col gap-4"> <input
<fieldset class="mb-4 flex flex-col gap-4"> type="email"
<label class="flex flex-col gap-1"> name="email"
Email class="rounded-sm bg-neutral-800 p-3"
<input placeholder="Email"
type="email" autocomplete="email"
name="email" value={userData.email}
class="rounded-sm bg-neutral-800 p-3" required
placeholder="Email" />
autocomplete="email" </label>
value={userData.email} <label class="flex flex-col gap-1">
required Password (leave blank for unchanged)
/> <input
</label> type="password"
<label class="flex flex-col gap-1"> name="newPassword"
Password (leave blank for unchanged) class="rounded-sm bg-neutral-800 p-3"
<input placeholder="Password"
type="password" autocomplete="new-password"
name="newPassword" />
class="rounded-sm bg-neutral-800 p-3" </label>
placeholder="Password" <label class="flex flex-col gap-1">
autocomplete="new-password" Current Password
/> <input
</label> type="password"
<label class="flex flex-col gap-1"> name="password"
Current Password class="rounded-sm bg-neutral-800 p-3"
<input placeholder="Password"
type="password" autocomplete="current-password"
name="password" required
class="rounded-sm bg-neutral-800 p-3" />
placeholder="Password" </label>
autocomplete="current-password" </fieldset>
required <div role="group">
/> <input type="submit" value="Update" class="w-full btn-primary" />
</label> </div>
</fieldset> </form>
<div role="group"> </article>
<input type="submit" value="Update" class="w-full btn-primary" /> </main>
</div> </>
</form> </BaseHtml>
</article> );
</main> },
</> {
</BaseHtml> auth: true,
); },
}, { )
auth: true
})
.post( .post(
"/account", "/account",
async function handler({ body, set, redirect, jwt, cookie: { auth } }) { async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
@@ -513,6 +518,6 @@ export const user = new Elysia()
newPassword: t.MaybeEmpty(t.String()), newPassword: t.MaybeEmpty(t.String()),
password: t.String(), password: t.String(),
}), }),
cookie: "session" cookie: "session",
}, },
); );