mirror of
https://github.com/C4illin/ConvertX.git
synced 2025-11-05 06:23:18 +00:00
Compare commits
173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8674557e42 | ||
|
|
87052ce105 | ||
|
|
98ee26f6e2 | ||
|
|
96e2c88465 | ||
|
|
d55ba218ff | ||
|
|
ae2455e73e | ||
|
|
b9fe32053c | ||
|
|
5cf3d74e03 | ||
|
|
2b92778f37 | ||
|
|
27d4da8941 | ||
|
|
2384e22c22 | ||
|
|
6690caeb1e | ||
|
|
c714ade3e2 | ||
|
|
e9e95c61e9 | ||
|
|
b1e0e68d9c | ||
|
|
5ce3706550 | ||
|
|
57e47e95c0 | ||
|
|
6d6bc6cfdd | ||
|
|
b44eb22e77 | ||
|
|
6edfbaa27d | ||
|
|
d669baeff4 | ||
|
|
ec1a7bc015 | ||
|
|
0805241a19 | ||
|
|
83f041daa2 | ||
|
|
55331a4496 | ||
|
|
b53f07e7a7 | ||
|
|
0eb89ae712 | ||
|
|
7dd153b02c | ||
|
|
6ccafeb3b0 | ||
|
|
b703903b22 | ||
|
|
9e66eab0a2 | ||
|
|
b272bf9504 | ||
|
|
56632f3500 | ||
|
|
2d9d8f8b4f | ||
|
|
65d4e0fbbe | ||
|
|
8182d12ea0 | ||
|
|
1c241d4cad | ||
|
|
874ff6ee00 | ||
|
|
e9f1219ad9 | ||
|
|
4811452aec | ||
|
|
382ebad35a | ||
|
|
85945256e7 | ||
|
|
c504692569 | ||
|
|
64a16036be | ||
|
|
b9f038386f | ||
|
|
945775e52b | ||
|
|
e7f3466736 | ||
|
|
ee80eeb18d | ||
|
|
34c7e0bd25 | ||
|
|
492dbd5617 | ||
|
|
0935bf66ce | ||
|
|
7389e0a059 | ||
|
|
c512b45f91 | ||
|
|
3ae2db5d9b | ||
|
|
0945b40a9c | ||
|
|
20b958e547 | ||
|
|
e7e146c6c9 | ||
|
|
005ad2d66b | ||
|
|
e5c3a8acc4 | ||
|
|
87ecbabd1f | ||
|
|
991c4e4ba8 | ||
|
|
87ccd8b44c | ||
|
|
83e6699ca6 | ||
|
|
c91523c038 | ||
|
|
1f73f036b2 | ||
|
|
1223fabfca | ||
|
|
8a42a39e69 | ||
|
|
22023bad25 | ||
|
|
db2f2d8f0a | ||
|
|
d0fa9ac408 | ||
|
|
776a97289b | ||
|
|
95340dd0eb | ||
|
|
7dcd74cc5f | ||
|
|
c5efac9423 | ||
|
|
cceca9a924 | ||
|
|
4d4c13a8d8 | ||
|
|
f8f90ebd69 | ||
|
|
648d5070e2 | ||
|
|
801cf28d1e | ||
|
|
fae2ba9c54 | ||
|
|
10d20a8786 | ||
|
|
c9bc1e237e | ||
|
|
48a76a46b3 | ||
|
|
4dcb796e1b | ||
|
|
5952103bcd | ||
|
|
fd05ee5cd5 | ||
|
|
7e7d238c7a | ||
|
|
755a4170f2 | ||
|
|
263ba415f5 | ||
|
|
317260098a | ||
|
|
5304e94b4e | ||
|
|
aab2b311cf | ||
|
|
baa7ea40e6 | ||
|
|
481a11b610 | ||
|
|
c09fe296b1 | ||
|
|
f023aae753 | ||
|
|
5cd9544b55 | ||
|
|
97c23ba65c | ||
|
|
0ffda40ac8 | ||
|
|
cb639907ee | ||
|
|
0166842b78 | ||
|
|
277a35b5df | ||
|
|
42124e08b2 | ||
|
|
13169574f0 | ||
|
|
6fb07f0d13 | ||
|
|
8121114ccb | ||
|
|
81881af1c1 | ||
|
|
bbb4117e9d | ||
|
|
d7ec8179d8 | ||
|
|
cef60afee3 | ||
|
|
ccf116acde | ||
|
|
f609984c90 | ||
|
|
663e654c80 | ||
|
|
31050dbf66 | ||
|
|
8918c418f4 | ||
|
|
56266e0da8 | ||
|
|
a3f5b5153a | ||
|
|
6b4c7a16e0 | ||
|
|
7c7756713b | ||
|
|
15d4233a82 | ||
|
|
a1411a7559 | ||
|
|
853a4c4f32 | ||
|
|
b05b0a14b0 | ||
|
|
ff680cb295 | ||
|
|
31e1a3124c | ||
|
|
70278ef0b6 | ||
|
|
0a33fb32e7 | ||
|
|
26f52a5122 | ||
|
|
8f8de4295a | ||
|
|
660b342f2e | ||
|
|
6306a99740 | ||
|
|
5ca6f45809 | ||
|
|
3ea3e1dd01 | ||
|
|
2fddfbe24a | ||
|
|
25df58ba82 | ||
|
|
249bccdc7d | ||
|
|
ec0e2db0e9 | ||
|
|
ef9b68e0da | ||
|
|
31789738fc | ||
|
|
5fa349a80e | ||
|
|
5dfd0f6f44 | ||
|
|
bfa6301570 | ||
|
|
3ea52c4faf | ||
|
|
391e62bfee | ||
|
|
4d1da58f74 | ||
|
|
6dec9ae93b | ||
|
|
b466a6de99 | ||
|
|
186681ef44 | ||
|
|
1e2273b7c4 | ||
|
|
8d17f59a58 | ||
|
|
d8fcd15aeb | ||
|
|
8cc0eee254 | ||
|
|
e4b69023d9 | ||
|
|
7d40890636 | ||
|
|
3ecd2c62ae | ||
|
|
16cabab0d0 | ||
|
|
3e1c9e147f | ||
|
|
5e7a0f5634 | ||
|
|
61b02206c0 | ||
|
|
e19a32fc6b | ||
|
|
1712fea1d3 | ||
|
|
1ac4808a64 | ||
|
|
84fd5367ce | ||
|
|
337cfdc15b | ||
|
|
b979bd4f13 | ||
|
|
3cab902752 | ||
|
|
1d0dd2a69f | ||
|
|
17f439210a | ||
|
|
9970fd3f89 | ||
|
|
8b7bcceb7b | ||
|
|
93ebdabf6f | ||
|
|
0b278c989b | ||
|
|
518e771afe |
@@ -12,17 +12,11 @@ const config = {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
tsconfigRootDir: __dirname,
|
||||
project: [
|
||||
"./tsconfig.json",
|
||||
"./cli/tsconfig.eslint.json", // separate eslint config for the CLI since we want to lint and typecheck differently due to template files
|
||||
"./upgrade/tsconfig.json",
|
||||
"./www/tsconfig.json",
|
||||
],
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
overrides: [
|
||||
// Template files don't have reliable type information
|
||||
{
|
||||
files: ["./cli/template/**/*.{ts,tsx}"],
|
||||
extends: ["plugin:@typescript-eslint/disable-type-checked"],
|
||||
},
|
||||
],
|
||||
@@ -46,9 +40,6 @@ const config = {
|
||||
// These lint rules don't make sense for us but are enabled in the preset configs
|
||||
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
|
||||
// This rule doesn't seem to be working properly
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "off",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
22
.github/dependabot.yml
vendored
22
.github/dependabot.yml
vendored
@@ -5,19 +5,19 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for npm
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
- package-ecosystem: npm
|
||||
versioning-strategy: increase
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# Maintain dependencies for Docker
|
||||
- package-ecosystem: "docker"
|
||||
interval: daily
|
||||
commit-message:
|
||||
prefix: "build"
|
||||
include: "scope"
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: "build"
|
||||
include: "scope"
|
||||
2
.github/workflows/bun-dependabot.yml
vendored
2
.github/workflows/bun-dependabot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
3
.github/workflows/docker-publish.yml
vendored
3
.github/workflows/docker-publish.yml
vendored
@@ -58,9 +58,10 @@ jobs:
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
1
.github/workflows/release-please.yml
vendored
1
.github/workflows/release-please.yml
vendored
@@ -19,6 +19,7 @@ jobs:
|
||||
# (PAT) and configured it as a GitHub action secret named
|
||||
# `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
|
||||
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# this is a built-in strategy in release-please, see "Action Inputs"
|
||||
# for more options
|
||||
release-type: node
|
||||
21
.github/workflows/remove-docker-tag.yml
vendored
Normal file
21
.github/workflows/remove-docker-tag.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Remove Docker Tag
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
remove-docker-tag:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
# (required)
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Remove Docker Tag
|
||||
uses: ArchieAtkinson/remove-dockertag-action@v0.0
|
||||
with:
|
||||
tag_name: master
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ package-lock.json
|
||||
/db
|
||||
/data
|
||||
/Bruno
|
||||
/tsconfig.tsbuildinfo
|
||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
52
CHANGELOG.md
52
CHANGELOG.md
@@ -1,5 +1,57 @@
|
||||
# Changelog
|
||||
|
||||
## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c))
|
||||
|
||||
## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
|
||||
|
||||
## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
|
||||
|
||||
## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
|
||||
* change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
|
||||
* print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
|
||||
|
||||
## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
|
||||
* change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
|
||||
|
||||
## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
|
||||
|
||||
## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
|
||||
|
||||
## 0.1.0 (2024-05-30)
|
||||
|
||||
|
||||
|
||||
63
Debian.Dockerfile
Normal file
63
Debian.Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
||||
FROM oven/bun:1-debian as base
|
||||
WORKDIR /app
|
||||
|
||||
# install dependencies into temp directory
|
||||
# this will cache them and speed up future builds
|
||||
FROM base AS install
|
||||
RUN mkdir -p /temp/dev
|
||||
COPY package.json bun.lockb /temp/dev/
|
||||
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||
|
||||
# install with --production (exclude devDependencies)
|
||||
RUN mkdir -p /temp/prod
|
||||
COPY package.json bun.lockb /temp/prod/
|
||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||
|
||||
# FROM base AS install-libjxl-tools
|
||||
# download
|
||||
|
||||
|
||||
|
||||
# copy node_modules from temp directory
|
||||
# then copy all (non-ignored) project files into the image
|
||||
# FROM base AS prerelease
|
||||
# COPY --from=install /temp/dev/node_modules node_modules
|
||||
# COPY . .
|
||||
|
||||
# # [optional] tests & build
|
||||
# ENV NODE_ENV=production
|
||||
# RUN bun test
|
||||
# RUN bun run build
|
||||
|
||||
# copy production dependencies and source code into final image
|
||||
FROM base AS release
|
||||
LABEL maintainer="Emrik Östling (C4illin)"
|
||||
LABEL description="ConvertX: self-hosted online file converter supporting 700+ file formats."
|
||||
LABEL repo="https://github.com/C4illin/ConvertX"
|
||||
|
||||
# install additional dependencies
|
||||
RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
|
||||
&& apt-get install -y \
|
||||
pandoc \
|
||||
texlive-latex-recommended \
|
||||
texlive-fonts-recommended \
|
||||
texlive-latex-extra \
|
||||
ffmpeg \
|
||||
graphicsmagick \
|
||||
ghostscript \
|
||||
libvips-tools
|
||||
|
||||
# # libjxl is not available in the official debian repositories
|
||||
# RUN wget https://github.com/libjxl/libjxl/releases/download/v0.10.2/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -O /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz \
|
||||
# && mkdir -p /tmp/libjxl \
|
||||
# && tar -xvf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -C /tmp/libjxl \
|
||||
# && dpkg -i /tmp/libjxl/libjxl_0.10.2_amd64.deb /tmp/libjxl/jxl_0.10.2_amd64.deb \
|
||||
# && rm -rf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz /tmp/libjxl
|
||||
|
||||
COPY --from=install /temp/prod/node_modules node_modules
|
||||
# COPY --from=prerelease /app/src/index.tsx /app/src/
|
||||
# COPY --from=prerelease /app/package.json .
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000/tcp
|
||||
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
|
||||
18
Dockerfile
18
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1-debian as base
|
||||
FROM oven/bun:1.1.21-alpine as base
|
||||
WORKDIR /app
|
||||
|
||||
# install dependencies into temp directory
|
||||
@@ -31,16 +31,19 @@ LABEL description="ConvertX: self-hosted online file converter supporting 700+ f
|
||||
LABEL repo="https://github.com/C4illin/ConvertX"
|
||||
|
||||
# install additional dependencies
|
||||
RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
|
||||
&& apt-get install -y \
|
||||
RUN apk --no-cache add \
|
||||
pandoc \
|
||||
texlive-latex-recommended \
|
||||
texlive-fonts-recommended \
|
||||
texlive-latex-extra \
|
||||
texlive \
|
||||
texlive-xetex \
|
||||
texmf-dist-latexextra \
|
||||
ffmpeg \
|
||||
graphicsmagick \
|
||||
ghostscript \
|
||||
libvips-tools
|
||||
vips-tools \
|
||||
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=prerelease /app/src/index.tsx /app/src/
|
||||
@@ -48,4 +51,5 @@ COPY --from=install /temp/prod/node_modules node_modules
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000/tcp
|
||||
ENV NODE_ENV=production
|
||||
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
|
||||
25
README.md
25
README.md
@@ -1,8 +1,13 @@
|
||||

|
||||
# ConvertX
|
||||
[](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
|
||||
[](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
A self-hosted online file converter. Supports 831 different formats. Written with Typescript, Bun and Elysia.
|
||||
A self-hosted online file converter. Supports 831 different formats. Written with TypeScript, Bun and Elysia.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -13,22 +18,25 @@ A self-hosted online file converter. Supports 831 different formats. Written wit
|
||||
## Converters supported
|
||||
|
||||
| Converter | Use case | Converts from | Converts to |
|
||||
|----------------|---------------|---------------|-------------|
|
||||
| Vips | Images (fast) | 45 | 23 |
|
||||
| PDFLaTeX | Documents | 1 | 1 |
|
||||
| Pandoc | Documents | 43 | 65 |
|
||||
| GraphicsMagick | Images | 166 | 133 |
|
||||
| FFmpeg | Video | ~473 | ~280 |
|
||||
|------------------------------------------------------------------------------|---------------|---------------|-------------|
|
||||
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
|
||||
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
|
||||
| [XeLaTeX](https://tug.org/xetex/) | Documents | 1 | 1 |
|
||||
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
||||
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
|
||||
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 |
|
||||
|
||||
<!-- many ffmpeg fileformats are duplicates -->
|
||||
|
||||
Any missing converter? Open an issue or pull request!
|
||||
|
||||
## Deployment
|
||||
|
||||
```yml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
convertx:
|
||||
image: ghcr.io/c4illin/convertx:main
|
||||
image: ghcr.io/c4illin/convertx
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment: # Defaults are listed below. All are optional.
|
||||
@@ -60,6 +68,7 @@ Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
|
||||
- [ ] Divide index.tsx into smaller components
|
||||
- [ ] Add tests
|
||||
- [ ] Add searchable list of formats
|
||||
- [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible.
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ services:
|
||||
convertx:
|
||||
build:
|
||||
context: .
|
||||
# dockerfile: Debian.Dockerfile
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
|
||||
49
package.json
49
package.json
@@ -1,39 +1,50 @@
|
||||
{
|
||||
"name": "convertx-frontend",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.3",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.tsx",
|
||||
"hot": "bun run --hot src/index.tsx",
|
||||
"format": "biome format --write ./src",
|
||||
"css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat"
|
||||
"css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat",
|
||||
"lint": "run-p 'lint:*'",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint:knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/cookie": "^0.8.0",
|
||||
"@elysiajs/html": "^1.0.2",
|
||||
"@elysiajs/jwt": "^1.0.2",
|
||||
"@elysiajs/static": "^1.0.3",
|
||||
"elysia": "^1.0.22"
|
||||
"@elysiajs/html": "1.0.2",
|
||||
"@elysiajs/jwt": "^1.1.0",
|
||||
"@elysiajs/static": "1.0.3",
|
||||
"elysia": "^1.1.4"
|
||||
},
|
||||
"module": "src/index.tsx",
|
||||
"type": "module",
|
||||
"bun-create": {
|
||||
"start": "bun run src/index.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.7.3",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
|
||||
"@kitajs/ts-html-plugin": "^4.0.1",
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@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.3",
|
||||
"@types/eslint": "^8.56.10",
|
||||
"@types/node": "^20.12.13",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@types/bun": "^1.1.6",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-isaacscript": "^3.12.2",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"knip": "^5.27.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@biomejs/biome"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const BaseHtml = ({ children, title = "ConvertX" }) => (
|
||||
export const BaseHtml = ({
|
||||
children,
|
||||
title = "ConvertX",
|
||||
}: { children: JSX.Element; title?: string }) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -30,7 +30,7 @@ export const Header = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="container">
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
@@ -260,6 +260,7 @@ export const properties = {
|
||||
"mpegts",
|
||||
"mpegtsraw",
|
||||
"mpegvideo",
|
||||
"mpg",
|
||||
"mpjpeg",
|
||||
"mpl2",
|
||||
"mpo",
|
||||
|
||||
71
src/converters/libjxl.ts
Normal file
71
src/converters/libjxl.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
// declare possible conversions
|
||||
export const properties = {
|
||||
from: {
|
||||
jxl: ["jxl"],
|
||||
images: [
|
||||
"apng",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg",
|
||||
"pam",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pgx",
|
||||
"png",
|
||||
"ppm",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
jxl: [
|
||||
"apng",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg",
|
||||
"pam",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pgx",
|
||||
"png",
|
||||
"ppm",
|
||||
],
|
||||
images: ["jxl"],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
options?: any,
|
||||
): Promise<string> {
|
||||
let tool = "";
|
||||
if (fileType === "jxl") {
|
||||
tool = "djxl";
|
||||
}
|
||||
|
||||
if (convertTo === "jxl") {
|
||||
tool = "cjxl";
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`${tool} "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("success");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -16,9 +16,14 @@ import {
|
||||
} from "./graphicsmagick";
|
||||
|
||||
import {
|
||||
convert as convertPdflatex,
|
||||
properties as propertiesPdflatex,
|
||||
} from "./pdflatex";
|
||||
convert as convertxelatex,
|
||||
properties as propertiesxelatex,
|
||||
} from "./xelatex";
|
||||
|
||||
import {
|
||||
convert as convertLibjxl,
|
||||
properties as propertiesLibjxl,
|
||||
} from "./libjxl";
|
||||
|
||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
||||
|
||||
@@ -50,13 +55,17 @@ const properties: {
|
||||
) => any;
|
||||
};
|
||||
} = {
|
||||
libjxl: {
|
||||
properties: propertiesLibjxl,
|
||||
converter: convertLibjxl,
|
||||
},
|
||||
vips: {
|
||||
properties: propertiesImage,
|
||||
converter: convertImage,
|
||||
},
|
||||
pdflatex: {
|
||||
properties: propertiesPdflatex,
|
||||
converter: convertPdflatex,
|
||||
xelatex: {
|
||||
properties: propertiesxelatex,
|
||||
converter: convertxelatex,
|
||||
},
|
||||
pandoc: {
|
||||
properties: propertiesPandoc,
|
||||
@@ -102,9 +111,8 @@ export async function mainConverter(
|
||||
|
||||
for (const key in converterObj.properties.from) {
|
||||
if (
|
||||
// HOW??
|
||||
converterObj.properties.from[key].includes(fileType) &&
|
||||
converterObj.properties.to[key].includes(convertTo)
|
||||
converterObj?.properties?.from[key]?.includes(fileType) &&
|
||||
converterObj?.properties?.to[key]?.includes(convertTo)
|
||||
) {
|
||||
converterFunc = converterObj.converter;
|
||||
break;
|
||||
@@ -193,7 +201,7 @@ for (const converterName in properties) {
|
||||
}
|
||||
possibleInputs.sort();
|
||||
|
||||
export const getPossibleInputs = () => {
|
||||
const getPossibleInputs = () => {
|
||||
return possibleInputs;
|
||||
};
|
||||
|
||||
@@ -208,9 +216,9 @@ for (const converterName in properties) {
|
||||
|
||||
for (const key in converterProperties.to) {
|
||||
if (allTargets[converterName]) {
|
||||
allTargets[converterName].push(...converterProperties.to[key]);
|
||||
allTargets[converterName].push(...(converterProperties.to[key] || []));
|
||||
} else {
|
||||
allTargets[converterName] = converterProperties.to[key];
|
||||
allTargets[converterName] = converterProperties.to[key] || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,9 +237,9 @@ for (const converterName in properties) {
|
||||
|
||||
for (const key in converterProperties.from) {
|
||||
if (allInputs[converterName]) {
|
||||
allInputs[converterName].push(...converterProperties.from[key]);
|
||||
allInputs[converterName].push(...(converterProperties.from[key] || []));
|
||||
} else {
|
||||
allInputs[converterName] = converterProperties.from[key];
|
||||
allInputs[converterName] = converterProperties.from[key] || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
import type { FormatEnum } from "sharp";
|
||||
|
||||
// declare possible conversions
|
||||
export const properties = {
|
||||
from: {
|
||||
images: [
|
||||
"avif",
|
||||
"bif",
|
||||
"csv",
|
||||
"exr",
|
||||
"fits",
|
||||
"gif",
|
||||
"hdr.gz",
|
||||
"hdr",
|
||||
"heic",
|
||||
"heif",
|
||||
"img.gz",
|
||||
"img",
|
||||
"j2c",
|
||||
"j2k",
|
||||
"jp2",
|
||||
"jpeg",
|
||||
"jpx",
|
||||
"jxl",
|
||||
"mat",
|
||||
"mrxs",
|
||||
"ndpi",
|
||||
"nia.gz",
|
||||
"nia",
|
||||
"nii.gz",
|
||||
"nii",
|
||||
"pdf",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pic",
|
||||
"png",
|
||||
"ppm",
|
||||
"raw",
|
||||
"scn",
|
||||
"svg",
|
||||
"svs",
|
||||
"svslide",
|
||||
"szi",
|
||||
"tif",
|
||||
"tiff",
|
||||
"v",
|
||||
"vips",
|
||||
"vms",
|
||||
"vmu",
|
||||
"webp",
|
||||
"zip",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
images: [
|
||||
"avif",
|
||||
"dzi",
|
||||
"fits",
|
||||
"gif",
|
||||
"hdr.gz",
|
||||
"heic",
|
||||
"heif",
|
||||
"img.gz",
|
||||
"j2c",
|
||||
"j2k",
|
||||
"jp2",
|
||||
"jpeg",
|
||||
"jpx",
|
||||
"jxl",
|
||||
"mat",
|
||||
"nia.gz",
|
||||
"nia",
|
||||
"nii.gz",
|
||||
"nii",
|
||||
"png",
|
||||
"tiff",
|
||||
"vips",
|
||||
"webp",
|
||||
],
|
||||
},
|
||||
options: {
|
||||
svg: {
|
||||
scale: {
|
||||
description: "Scale the image up or down",
|
||||
type: "number",
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: keyof FormatEnum,
|
||||
targetPath: string,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
options?: any,
|
||||
) {
|
||||
if (fileType === "svg") {
|
||||
const scale = options.scale || 1;
|
||||
const metadata = await sharp(filePath).metadata();
|
||||
|
||||
if (!metadata || !metadata.width || !metadata.height) {
|
||||
throw new Error("Could not get metadata from image");
|
||||
}
|
||||
|
||||
const newWidth = Math.round(metadata.width * scale);
|
||||
const newHeight = Math.round(metadata.height * scale);
|
||||
|
||||
return await sharp(filePath)
|
||||
.resize(newWidth, newHeight)
|
||||
.toFormat(convertTo)
|
||||
.toFile(targetPath);
|
||||
}
|
||||
|
||||
return await sharp(filePath).toFormat(convertTo).toFile(targetPath);
|
||||
}
|
||||
@@ -127,9 +127,15 @@ export function convert(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
options?: any,
|
||||
): Promise<string> {
|
||||
// set xelatex here
|
||||
const xelatex = ["pdf", "latex"];
|
||||
let option = "";
|
||||
if (xelatex.includes(convertTo)) {
|
||||
option = "--pdf-engine=xelatex";
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
`pandoc "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
|
||||
`pandoc ${option} "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
|
||||
@@ -19,9 +19,13 @@ export function convert(
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
|
||||
const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "")
|
||||
const outputPath = targetPath
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")
|
||||
.replace("./", "");
|
||||
exec(
|
||||
`pdflatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
|
||||
`latexmk -xelatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
85
src/helpers/printVersions.ts
Normal file
85
src/helpers/printVersions.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { version } from "../../package.json";
|
||||
console.log(`ConvertX v${version}`);
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
exec("cat /etc/os-release", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("Not running on docker, this is not supported.");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split('PRETTY_NAME="')[1]?.split('"')[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("pandoc -v", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("Pandoc is not installed.");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("ffmpeg -version", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("FFmpeg is not installed.");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("vips -v", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("Vips is not installed.");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("gm version", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("GraphicsMagick is not installed.");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("djxl --version", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("libjxl-tools is not installed.");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("xelatex -version", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("Tex Live with XeTeX is not installed.");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("bun -v", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("Bun is not installed. wait what");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`Bun v${stdout.split("\n")[0]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
218
src/index.tsx
218
src/index.tsx
@@ -19,7 +19,9 @@ import {
|
||||
normalizeFiletype,
|
||||
normalizeOutputFiletype,
|
||||
} from "./helpers/normalizeFiletype";
|
||||
import "./helpers/printVersions";
|
||||
|
||||
mkdir("./data", { recursive: true }).catch(console.error);
|
||||
const db = new Database("./data/mydb.sqlite", { create: true });
|
||||
const uploadsDir = "./data/uploads/";
|
||||
const outputDir = "./data/output/";
|
||||
@@ -74,33 +76,37 @@ if (dbVersion === 0) {
|
||||
|
||||
let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
|
||||
|
||||
interface IUser {
|
||||
id: number;
|
||||
email: string;
|
||||
password: string;
|
||||
class User {
|
||||
id!: number;
|
||||
email!: string;
|
||||
password!: string;
|
||||
}
|
||||
|
||||
interface IFileNames {
|
||||
id: number;
|
||||
job_id: number;
|
||||
file_name: string;
|
||||
output_file_name: string;
|
||||
status: string;
|
||||
class Filename {
|
||||
id!: number;
|
||||
job_id!: number;
|
||||
file_name!: string;
|
||||
output_file_name!: string;
|
||||
status!: string;
|
||||
}
|
||||
|
||||
interface IJobs {
|
||||
finished_files: number;
|
||||
id: number;
|
||||
user_id: number;
|
||||
date_created: string;
|
||||
status: string;
|
||||
num_files: number;
|
||||
class Jobs {
|
||||
finished_files!: number;
|
||||
id!: number;
|
||||
user_id!: number;
|
||||
date_created!: string;
|
||||
status!: string;
|
||||
num_files!: number;
|
||||
}
|
||||
|
||||
// enable WAL mode
|
||||
db.exec("PRAGMA journal_mode = WAL;");
|
||||
|
||||
const app = new Elysia()
|
||||
const app = new Elysia({
|
||||
serve: {
|
||||
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
})
|
||||
.use(cookie())
|
||||
.use(html())
|
||||
.use(
|
||||
@@ -121,7 +127,7 @@ const app = new Elysia()
|
||||
)
|
||||
.get("/setup", ({ redirect }) => {
|
||||
if (!FIRST_RUN) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -164,11 +170,12 @@ const app = new Elysia()
|
||||
})
|
||||
.get("/register", ({ redirect }) => {
|
||||
if (!ACCOUNT_REGISTRATION) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Register">
|
||||
<>
|
||||
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
||||
<main class="container">
|
||||
<article>
|
||||
@@ -199,6 +206,7 @@ const app = new Elysia()
|
||||
</form>
|
||||
</article>
|
||||
</main>
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
})
|
||||
@@ -206,7 +214,7 @@ const app = new Elysia()
|
||||
"/register",
|
||||
async ({ body, set, redirect, jwt, cookie: { auth } }) => {
|
||||
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
if (FIRST_RUN) {
|
||||
@@ -229,9 +237,17 @@ const app = new Elysia()
|
||||
savedPassword,
|
||||
);
|
||||
|
||||
const user = (await db
|
||||
const user = db
|
||||
.query("SELECT * FROM users WHERE email = ?")
|
||||
.get(body.email)) as IUser;
|
||||
.as(User)
|
||||
.get(body.email);
|
||||
|
||||
if (!user) {
|
||||
set.status = 500;
|
||||
return {
|
||||
message: "Failed to create user.",
|
||||
};
|
||||
}
|
||||
|
||||
const accessToken = await jwt.sign({
|
||||
id: String(user.id),
|
||||
@@ -253,13 +269,13 @@ const app = new Elysia()
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
},
|
||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||
)
|
||||
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (FIRST_RUN) {
|
||||
return redirect("/setup");
|
||||
return redirect("/setup", 302);
|
||||
}
|
||||
|
||||
// if already logged in, redirect to home
|
||||
@@ -267,7 +283,7 @@ const app = new Elysia()
|
||||
const user = await jwt.verify(auth.value);
|
||||
|
||||
if (user) {
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
}
|
||||
|
||||
auth.remove();
|
||||
@@ -275,6 +291,7 @@ const app = new Elysia()
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Login">
|
||||
<>
|
||||
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
||||
<main class="container">
|
||||
<article>
|
||||
@@ -312,15 +329,17 @@ const app = new Elysia()
|
||||
</form>
|
||||
</article>
|
||||
</main>
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
})
|
||||
.post(
|
||||
"/login",
|
||||
async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
|
||||
const existingUser = (await db
|
||||
const existingUser = await db
|
||||
.query("SELECT * FROM users WHERE email = ?")
|
||||
.get(body.email)) as IUser;
|
||||
.as(User)
|
||||
.get(body.email);
|
||||
|
||||
if (!existingUser) {
|
||||
set.status = 403;
|
||||
@@ -361,7 +380,7 @@ const app = new Elysia()
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
},
|
||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||
)
|
||||
@@ -370,39 +389,40 @@ const app = new Elysia()
|
||||
auth.remove();
|
||||
}
|
||||
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
})
|
||||
.post("/logoff", ({ redirect, cookie: { auth } }) => {
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
})
|
||||
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
||||
if (FIRST_RUN) {
|
||||
return redirect("/setup");
|
||||
return redirect("/setup", 302);
|
||||
}
|
||||
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
// validate jwt
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
// make sure user exists in db
|
||||
const existingUser = (await db
|
||||
const existingUser = await db
|
||||
.query("SELECT * FROM users WHERE id = ?")
|
||||
.get(user.id)) as IUser;
|
||||
.as(User)
|
||||
.get(user.id);
|
||||
|
||||
if (!existingUser) {
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
// create a new job
|
||||
@@ -433,6 +453,7 @@ const app = new Elysia()
|
||||
|
||||
return (
|
||||
<BaseHtml>
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<main class="container">
|
||||
<article>
|
||||
@@ -459,7 +480,8 @@ const app = new Elysia()
|
||||
<option selected disabled value="">
|
||||
Convert to
|
||||
</option>
|
||||
{Object.entries(getAllTargets()).map(([converter, targets]) => (
|
||||
{Object.entries(getAllTargets()).map(
|
||||
([converter, targets]) => (
|
||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||
<optgroup label={converter}>
|
||||
{targets.map((target) => (
|
||||
@@ -469,13 +491,15 @@ const app = new Elysia()
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
</article>
|
||||
<input type="submit" value="Convert" />
|
||||
</form>
|
||||
</main>
|
||||
<script src="script.js" defer />
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
})
|
||||
@@ -509,16 +533,16 @@ const app = new Elysia()
|
||||
"/upload",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
@@ -526,7 +550,7 @@ const app = new Elysia()
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -557,16 +581,16 @@ const app = new Elysia()
|
||||
"/delete",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
@@ -574,7 +598,7 @@ const app = new Elysia()
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -587,24 +611,25 @@ const app = new Elysia()
|
||||
"/convert",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
}
|
||||
|
||||
const existingJob = (await db
|
||||
const existingJob = await db
|
||||
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
||||
.get(jobId.value, user.id)) as IJobs;
|
||||
.as(Jobs)
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -627,17 +652,15 @@ const app = new Elysia()
|
||||
const fileNames = JSON.parse(body.file_names) as string[];
|
||||
|
||||
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
||||
return redirect("/");
|
||||
return redirect("/", 302);
|
||||
}
|
||||
|
||||
db.run(
|
||||
"UPDATE jobs SET num_files = ?, status = 'pending' WHERE id = ?",
|
||||
fileNames.length,
|
||||
jobId.value,
|
||||
);
|
||||
db.query(
|
||||
"UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2",
|
||||
).run(fileNames.length, jobId.value);
|
||||
|
||||
const query = db.query(
|
||||
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?, ?, ?, ?)",
|
||||
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
|
||||
);
|
||||
|
||||
// Start the conversion process in the background
|
||||
@@ -658,16 +681,18 @@ const app = new Elysia()
|
||||
{},
|
||||
converterName,
|
||||
);
|
||||
|
||||
if (jobId.value) {
|
||||
query.run(jobId.value, fileName, newFileName, result);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
// All conversions are done, update the job status to 'completed'
|
||||
db.run(
|
||||
"UPDATE jobs SET status = 'completed' WHERE id = ?",
|
||||
if (jobId.value) {
|
||||
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(
|
||||
jobId.value,
|
||||
);
|
||||
}
|
||||
|
||||
// delete all uploaded files in userUploadsDir
|
||||
// rmSync(userUploadsDir, { recursive: true, force: true });
|
||||
@@ -677,7 +702,7 @@ const app = new Elysia()
|
||||
});
|
||||
|
||||
// Redirect the client immediately
|
||||
return redirect(`/results/${jobId.value}`);
|
||||
return redirect(`/results/${jobId.value}`, 302);
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
@@ -688,22 +713,24 @@ const app = new Elysia()
|
||||
)
|
||||
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
const user = await jwt.verify(auth.value);
|
||||
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
let userJobs = db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ?")
|
||||
.all(user.id) as IJobs[];
|
||||
.as(Jobs)
|
||||
.all(user.id);
|
||||
|
||||
for (const job of userJobs) {
|
||||
const files = db
|
||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||
.all(job.id) as IFileNames[];
|
||||
.as(Filename)
|
||||
.all(job.id);
|
||||
|
||||
job.finished_files = files.length;
|
||||
}
|
||||
@@ -713,6 +740,7 @@ const app = new Elysia()
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Results">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<main class="container">
|
||||
<article>
|
||||
@@ -744,6 +772,7 @@ const app = new Elysia()
|
||||
</table>
|
||||
</article>
|
||||
</main>
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
})
|
||||
@@ -751,7 +780,7 @@ const app = new Elysia()
|
||||
"/results/:jobId",
|
||||
async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
if (job_id?.value) {
|
||||
@@ -761,12 +790,13 @@ const app = new Elysia()
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const job = (await db
|
||||
const job = await db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.get(user.id, params.jobId)) as IJobs;
|
||||
.as(Jobs)
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
set.status = 404;
|
||||
@@ -779,10 +809,12 @@ const app = new Elysia()
|
||||
|
||||
const files = db
|
||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||
.all(params.jobId) as IFileNames[];
|
||||
.as(Filename)
|
||||
.all(params.jobId);
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Result">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<main class="container">
|
||||
<article>
|
||||
@@ -838,6 +870,7 @@ const app = new Elysia()
|
||||
</article>
|
||||
</main>
|
||||
<script src="/results.js" defer />
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
},
|
||||
@@ -846,7 +879,7 @@ const app = new Elysia()
|
||||
"/progress/:jobId",
|
||||
async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
if (job_id?.value) {
|
||||
@@ -856,12 +889,13 @@ const app = new Elysia()
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const job = (await db
|
||||
const job = await db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.get(user.id, params.jobId)) as IJobs;
|
||||
.as(Jobs)
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
set.status = 404;
|
||||
@@ -874,7 +908,8 @@ const app = new Elysia()
|
||||
|
||||
const files = db
|
||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||
.all(params.jobId) as IFileNames[];
|
||||
.as(Filename)
|
||||
.all(params.jobId);
|
||||
|
||||
return (
|
||||
<article>
|
||||
@@ -934,12 +969,12 @@ const app = new Elysia()
|
||||
"/download/:userId/:jobId/:fileName",
|
||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const job = await db
|
||||
@@ -947,7 +982,7 @@ const app = new Elysia()
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect("/results");
|
||||
return redirect("/results", 302);
|
||||
}
|
||||
// parse from url encoded string
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
@@ -960,16 +995,17 @@ const app = new Elysia()
|
||||
)
|
||||
.get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Converters">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<main class="container">
|
||||
<article>
|
||||
@@ -983,7 +1019,8 @@ const app = new Elysia()
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(getAllTargets()).map(([converter, targets]) => {
|
||||
{Object.entries(getAllTargets()).map(
|
||||
([converter, targets]) => {
|
||||
const inputs = getAllInputs(converter);
|
||||
return (
|
||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||
@@ -1009,11 +1046,13 @@ const app = new Elysia()
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
},
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</main>
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
})
|
||||
@@ -1022,12 +1061,12 @@ const app = new Elysia()
|
||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||
// TODO: Implement zip download
|
||||
if (!auth?.value) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
|
||||
const job = await db
|
||||
@@ -1035,7 +1074,7 @@ const app = new Elysia()
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect("/results");
|
||||
return redirect("/results", 302);
|
||||
}
|
||||
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
@@ -1060,7 +1099,8 @@ const clearJobs = () => {
|
||||
// get all files older than 24 hours
|
||||
const jobs = db
|
||||
.query("SELECT * FROM jobs WHERE date_created < ?")
|
||||
.all(new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()) as IJobs[];
|
||||
.as(Jobs)
|
||||
.all(new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
|
||||
|
||||
for (const job of jobs) {
|
||||
// delete the directories
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
article {
|
||||
/* height: 300px; */
|
||||
/* width: 300px; */
|
||||
|
||||
}
|
||||
|
||||
div.icon {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
],
|
||||
// non bun init
|
||||
"plugins": [{ "name": "@kitajs/ts-html-plugin" }],
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
Reference in New Issue
Block a user