chore: format all files

This commit is contained in:
C4illin 2025-06-03 19:19:28 +02:00
parent ff2c0057e8
commit 1be11708c4
45 changed files with 2828 additions and 3057 deletions

View file

@ -1,164 +1,164 @@
name: Docker name: Docker
# thanks to https://github.com/sredevopsorg/multi-arch-docker-github-workflow # thanks to https://github.com/sredevopsorg/multi-arch-docker-github-workflow
on: on:
push: push:
branches: ["main"] branches: ["main"]
tags: ["v*.*.*"] tags: ["v*.*.*"]
pull_request: pull_request:
branches: ["main"] branches: ["main"]
workflow_dispatch: workflow_dispatch:
env: env:
GHCR_IMAGE: ghcr.io/c4illin/convertx GHCR_IMAGE: ghcr.io/c4illin/convertx
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: c4illin DOCKERHUB_USERNAME: c4illin
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
# The build job builds the Docker image for each platform specified in the matrix. # The build job builds the Docker image for each platform specified in the matrix.
build: build:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: platform:
- linux/amd64 - linux/amd64
- linux/arm64 - linux/arm64
permissions: permissions:
contents: write contents: write
packages: write packages: write
attestations: write attestations: write
checks: write checks: write
actions: read actions: read
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }} runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
name: Build Docker image for ${{ matrix.platform }} name: Build Docker image for ${{ matrix.platform }}
steps: steps:
- name: Prepare environment for current platform - name: Prepare environment for current platform
# This step sets up the environment for the current platform being built. # This step sets up the environment for the current platform being built.
# It replaces the '/' character in the platform name with '-' and sets it as an environment variable. # It replaces the '/' character in the platform name with '-' and sets it as an environment variable.
# This is useful for naming artifacts and other resources that cannot contain '/'. # This is useful for naming artifacts and other resources that cannot contain '/'.
# The environment variable PLATFORMS_PAIR will be used later in the workflow. # The environment variable PLATFORMS_PAIR will be used later in the workflow.
id: prepare id: prepare
run: | run: |
platform=${{ matrix.platform }} platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Docker meta default - name: Docker meta default
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.GHCR_IMAGE }} images: ${{ env.GHCR_IMAGE }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.10.0
with: with:
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
# here we only login to ghcr.io since the this only pushes internal images # here we only login to ghcr.io since the this only pushes internal images
uses: docker/login-action@v3.4.0 uses: docker/login-action@v3.4.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest - name: Build and push by digest
id: build id: build
uses: docker/build-push-action@v6.18.0 uses: docker/build-push-action@v6.18.0
env: env:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
with: with:
context: . context: .
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }} annotations: ${{ steps.meta.outputs.annotations }}
outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true
cache-from: type=gha,scope=${{ matrix.platform }} cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }} cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
- name: Export digest - name: Export digest
run: | run: |
mkdir -p /tmp/digests mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}" digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}" touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: digests-${{ env.PLATFORM_PAIR }} name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/* path: /tmp/digests/*
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
merge: merge:
name: Merge Docker manifests name: Merge Docker manifests
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
attestations: write attestations: write
contents: read contents: read
packages: write packages: write
needs: needs:
- build - build
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digests-* pattern: digests-*
merge-multiple: true merge-multiple: true
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: | images: |
${{ env.GHCR_IMAGE }} ${{ env.GHCR_IMAGE }}
${{ env.IMAGE_NAME }} ${{ env.IMAGE_NAME }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ env.DOCKERHUB_USERNAME }} username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get execution timestamp with RFC3339 format - name: Get execution timestamp with RFC3339 format
id: timestamp id: timestamp
run: | run: |
echo "timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT echo "timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT
- name: Create manifest list and push - name: Create manifest list and push
working-directory: /tmp/digests working-directory: /tmp/digests
run: | run: |
docker buildx imagetools create \ docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
--annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \ --annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \
--annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \ --annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \
--annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \ --annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \
--annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \ --annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \
$(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *) $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)
- name: Inspect image - name: Inspect image
run: | run: |
docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}' docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}'

View file

@ -15,13 +15,13 @@ jobs:
dockerHubDescription: dockerHubDescription:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Docker Hub Description - name: Docker Hub Description
uses: peter-evans/dockerhub-description@v4 uses: peter-evans/dockerhub-description@v4
with: with:
username: ${{ env.DOCKERHUB_USERNAME }} username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.IMAGE_NAME }} repository: ${{ env.IMAGE_NAME }}
short-description: ${{ github.event.repository.description }} short-description: ${{ github.event.repository.description }}
enable-url-completion: true enable-url-completion: true

View file

@ -14,8 +14,8 @@ jobs:
packages: write packages: write
steps: steps:
- name: Remove Docker Tag - name: Remove Docker Tag
uses: ArchieAtkinson/remove-dockertag-action@v0.0 uses: ArchieAtkinson/remove-dockertag-action@v0.0
with: with:
tag_name: master tag_name: master
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,4 +1,4 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true "typescript.enablePromptUseWorkspaceTsdk": true
} }

View file

@ -2,243 +2,209 @@
## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14) ## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14)
### Features ### Features
* add HIDE_HISTORY option to control visibility of history page ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf)) - add HIDE_HISTORY option to control visibility of history page ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf))
* add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da)) - add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da))
* Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f)) - Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f))
* add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1)) - add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1))
### Bug Fixes ### Bug Fixes
* add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258) - add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258)
## [0.12.1](https://github.com/C4illin/ConvertX/compare/v0.12.0...v0.12.1) (2025-03-20) ## [0.12.1](https://github.com/C4illin/ConvertX/compare/v0.12.0...v0.12.1) (2025-03-20)
### Bug Fixes ### Bug Fixes
* rollback to bun 1.2.2 ([cdae798](https://github.com/C4illin/ConvertX/commit/cdae798fcf5879e4adea87386a38748b9a1e1ddc)) - rollback to bun 1.2.2 ([cdae798](https://github.com/C4illin/ConvertX/commit/cdae798fcf5879e4adea87386a38748b9a1e1ddc))
## [0.12.0](https://github.com/C4illin/ConvertX/compare/v0.11.1...v0.12.0) (2025-03-06) ## [0.12.0](https://github.com/C4illin/ConvertX/compare/v0.11.1...v0.12.0) (2025-03-06)
### Features ### Features
* added progress bar for file upload ([db60f35](https://github.com/C4illin/ConvertX/commit/db60f355b2973f43f8e5990e6fe4e351b959b659)) - added progress bar for file upload ([db60f35](https://github.com/C4illin/ConvertX/commit/db60f355b2973f43f8e5990e6fe4e351b959b659))
* made every upload file independent ([cc54bdc](https://github.com/C4illin/ConvertX/commit/cc54bdcbe764c41cc3273485d072fd3178ad2dca)) - made every upload file independent ([cc54bdc](https://github.com/C4illin/ConvertX/commit/cc54bdcbe764c41cc3273485d072fd3178ad2dca))
* replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3)) - replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3))
### Bug Fixes ### Bug Fixes
* add libheif ([6b92540](https://github.com/C4illin/ConvertX/commit/6b9254047c0598963aee1d99e20ba1650a0368bf)) - add libheif ([6b92540](https://github.com/C4illin/ConvertX/commit/6b9254047c0598963aee1d99e20ba1650a0368bf))
* add libheif ([decfea5](https://github.com/C4illin/ConvertX/commit/decfea5dc9627b216bb276a9e1578c32cfa1deb6)), closes [#202](https://github.com/C4illin/ConvertX/issues/202) - add libheif ([decfea5](https://github.com/C4illin/ConvertX/commit/decfea5dc9627b216bb276a9e1578c32cfa1deb6)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
* added onerror log ([ae4bbc8](https://github.com/C4illin/ConvertX/commit/ae4bbc8baacbaf67763c62ea44140bb21cc17230)) - added onerror log ([ae4bbc8](https://github.com/C4illin/ConvertX/commit/ae4bbc8baacbaf67763c62ea44140bb21cc17230))
* refactored uploadFile to only accept a single file instead of multiple ([dc82a43](https://github.com/C4illin/ConvertX/commit/dc82a438d4104b79ff423d502a6779a43928968a)) - refactored uploadFile to only accept a single file instead of multiple ([dc82a43](https://github.com/C4illin/ConvertX/commit/dc82a438d4104b79ff423d502a6779a43928968a))
* update libheif to 1.19.5 ([fba5e21](https://github.com/C4illin/ConvertX/commit/fba5e212e8d0eaba8971e239e35aeb521f3cd813)), closes [#202](https://github.com/C4illin/ConvertX/issues/202) - update libheif to 1.19.5 ([fba5e21](https://github.com/C4illin/ConvertX/commit/fba5e212e8d0eaba8971e239e35aeb521f3cd813)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
## [0.11.1](https://github.com/C4illin/ConvertX/compare/v0.11.0...v0.11.1) (2025-02-07) ## [0.11.1](https://github.com/C4illin/ConvertX/compare/v0.11.0...v0.11.1) (2025-02-07)
### Bug Fixes ### Bug Fixes
* mobile view overflow ([bec58ac](https://github.com/C4illin/ConvertX/commit/bec58ac59f9600e35385b9e21d174f3ab1b42b1d)) - mobile view overflow ([bec58ac](https://github.com/C4illin/ConvertX/commit/bec58ac59f9600e35385b9e21d174f3ab1b42b1d))
## [0.11.0](https://github.com/C4illin/ConvertX/compare/v0.10.1...v0.11.0) (2025-02-05) ## [0.11.0](https://github.com/C4illin/ConvertX/compare/v0.10.1...v0.11.0) (2025-02-05)
### Features ### Features
* add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192) - add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192)
### Bug Fixes ### Bug Fixes
* don't crash if file is not found ([16f27c1](https://github.com/C4illin/ConvertX/commit/16f27c13bbc1c0e5fa2316f3db11d0918524053b)) - don't crash if file is not found ([16f27c1](https://github.com/C4illin/ConvertX/commit/16f27c13bbc1c0e5fa2316f3db11d0918524053b))
* install numpy for inkscape ([0e61051](https://github.com/C4illin/ConvertX/commit/0e61051fc6be188164c3865b4fb579c140859fdc)) - install numpy for inkscape ([0e61051](https://github.com/C4illin/ConvertX/commit/0e61051fc6be188164c3865b4fb579c140859fdc))
## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21) ## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21)
### Bug Fixes ### Bug Fixes
* ffmpeg works without ffmpeg_args ([3b7ea88](https://github.com/C4illin/ConvertX/commit/3b7ea88b7382f7c21b120bdc9bda5bb10547f55d)), closes [#212](https://github.com/C4illin/ConvertX/issues/212) - ffmpeg works without ffmpeg_args ([3b7ea88](https://github.com/C4illin/ConvertX/commit/3b7ea88b7382f7c21b120bdc9bda5bb10547f55d)), closes [#212](https://github.com/C4illin/ConvertX/issues/212)
## [0.10.0](https://github.com/C4illin/ConvertX/compare/v0.9.0...v0.10.0) (2025-01-18) ## [0.10.0](https://github.com/C4illin/ConvertX/compare/v0.9.0...v0.10.0) (2025-01-18)
### Features ### Features
* add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191) - add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191)
### Bug Fixes ### Bug Fixes
* add FFMPEG_ARGS env variable ([f537c81](https://github.com/C4illin/ConvertX/commit/f537c81db7815df8017f834e3162291197e1c40f)), closes [#190](https://github.com/C4illin/ConvertX/issues/190) - add FFMPEG_ARGS env variable ([f537c81](https://github.com/C4illin/ConvertX/commit/f537c81db7815df8017f834e3162291197e1c40f)), closes [#190](https://github.com/C4illin/ConvertX/issues/190)
* add qt6-qtbase-private-dev from community repo ([95dbc9f](https://github.com/C4illin/ConvertX/commit/95dbc9f678bec7e6e2c03587e1473fb8ff708ea3)) - add qt6-qtbase-private-dev from community repo ([95dbc9f](https://github.com/C4illin/ConvertX/commit/95dbc9f678bec7e6e2c03587e1473fb8ff708ea3))
* skip account setup when ALLOW_UNAUTHENTICATED is true ([538c5b6](https://github.com/C4illin/ConvertX/commit/538c5b60c9e27a8184740305475245da79bae143)) - skip account setup when ALLOW_UNAUTHENTICATED is true ([538c5b6](https://github.com/C4illin/ConvertX/commit/538c5b60c9e27a8184740305475245da79bae143))
## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21) ## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21)
### Features ### Features
* add inkscape for vector images ([f3740e9](https://github.com/C4illin/ConvertX/commit/f3740e9ded100b8500f3613517960248bbd3c210)) - add inkscape for vector images ([f3740e9](https://github.com/C4illin/ConvertX/commit/f3740e9ded100b8500f3613517960248bbd3c210))
* Allow to chose webroot ([36cb6cc](https://github.com/C4illin/ConvertX/commit/36cb6cc589d80d0a87fa8dbe605db71a9a2570f9)), closes [#180](https://github.com/C4illin/ConvertX/issues/180) - Allow to chose webroot ([36cb6cc](https://github.com/C4illin/ConvertX/commit/36cb6cc589d80d0a87fa8dbe605db71a9a2570f9)), closes [#180](https://github.com/C4illin/ConvertX/issues/180)
* disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177) - disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
### Bug Fixes ### Bug Fixes
* treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178) - treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178)
* wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177) - wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05) ## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05)
### Bug Fixes ### Bug Fixes
* disable convert button when input is empty ([78844d7](https://github.com/C4illin/ConvertX/commit/78844d7bd55990789ed07c81e49043e688cbe656)), closes [#151](https://github.com/C4illin/ConvertX/issues/151) - disable convert button when input is empty ([78844d7](https://github.com/C4illin/ConvertX/commit/78844d7bd55990789ed07c81e49043e688cbe656)), closes [#151](https://github.com/C4illin/ConvertX/issues/151)
* resize to fit for ico ([b4e53db](https://github.com/C4illin/ConvertX/commit/b4e53dbb8e70b3a95b44e5b756759d16117a87e1)), closes [#157](https://github.com/C4illin/ConvertX/issues/157) - resize to fit for ico ([b4e53db](https://github.com/C4illin/ConvertX/commit/b4e53dbb8e70b3a95b44e5b756759d16117a87e1)), closes [#157](https://github.com/C4illin/ConvertX/issues/157)
* treat jfif as jpeg ([339b79f](https://github.com/C4illin/ConvertX/commit/339b79f786131deb93f0d5683e03178fdcab1ef5)), closes [#163](https://github.com/C4illin/ConvertX/issues/163) - treat jfif as jpeg ([339b79f](https://github.com/C4illin/ConvertX/commit/339b79f786131deb93f0d5683e03178fdcab1ef5)), closes [#163](https://github.com/C4illin/ConvertX/issues/163)
## [0.8.0](https://github.com/C4illin/ConvertX/compare/v0.7.0...v0.8.0) (2024-09-30) ## [0.8.0](https://github.com/C4illin/ConvertX/compare/v0.7.0...v0.8.0) (2024-09-30)
### Features ### Features
* add light theme, fixes [#156](https://github.com/C4illin/ConvertX/issues/156) ([72636c5](https://github.com/C4illin/ConvertX/commit/72636c5059ebf09c8fece2e268293650b2f8ccf6)) - add light theme, fixes [#156](https://github.com/C4illin/ConvertX/issues/156) ([72636c5](https://github.com/C4illin/ConvertX/commit/72636c5059ebf09c8fece2e268293650b2f8ccf6))
### Bug Fixes ### Bug Fixes
* add support for usd for assimp, [#144](https://github.com/C4illin/ConvertX/issues/144) ([2057167](https://github.com/C4illin/ConvertX/commit/20571675766209ad1251f07e687d29a6791afc8b)) - add support for usd for assimp, [#144](https://github.com/C4illin/ConvertX/issues/144) ([2057167](https://github.com/C4illin/ConvertX/commit/20571675766209ad1251f07e687d29a6791afc8b))
* cleanup formats and add opus, fixes [#159](https://github.com/C4illin/ConvertX/issues/159) ([ae1dfaf](https://github.com/C4illin/ConvertX/commit/ae1dfafc9d9116a57b08c2f7fc326990e00824b0)) - cleanup formats and add opus, fixes [#159](https://github.com/C4illin/ConvertX/issues/159) ([ae1dfaf](https://github.com/C4illin/ConvertX/commit/ae1dfafc9d9116a57b08c2f7fc326990e00824b0))
* support .awb and clean up, fixes [#153](https://github.com/C4illin/ConvertX/issues/153), [#92](https://github.com/C4illin/ConvertX/issues/92) ([1c9e67f](https://github.com/C4illin/ConvertX/commit/1c9e67fc3201e0e5dee91e8981adf34daaabf33a)) - support .awb and clean up, fixes [#153](https://github.com/C4illin/ConvertX/issues/153), [#92](https://github.com/C4illin/ConvertX/issues/92) ([1c9e67f](https://github.com/C4illin/ConvertX/commit/1c9e67fc3201e0e5dee91e8981adf34daaabf33a))
## [0.7.0](https://github.com/C4illin/ConvertX/compare/v0.6.0...v0.7.0) (2024-09-26) ## [0.7.0](https://github.com/C4illin/ConvertX/compare/v0.6.0...v0.7.0) (2024-09-26)
### Features ### Features
* Add support for 3d assets through assimp converter ([63a4328](https://github.com/C4illin/ConvertX/commit/63a4328d4a1e01df3e0ec4a877bad8c8ffe71129)) - Add support for 3d assets through assimp converter ([63a4328](https://github.com/C4illin/ConvertX/commit/63a4328d4a1e01df3e0ec4a877bad8c8ffe71129))
### Bug Fixes ### Bug Fixes
* wrong layout on search with few options ([8817389](https://github.com/C4illin/ConvertX/commit/88173891ba2d69da46eda46f3f598a9b54f26f96)) - wrong layout on search with few options ([8817389](https://github.com/C4illin/ConvertX/commit/88173891ba2d69da46eda46f3f598a9b54f26f96))
## [0.6.0](https://github.com/C4illin/ConvertX/compare/v0.5.0...v0.6.0) (2024-09-25) ## [0.6.0](https://github.com/C4illin/ConvertX/compare/v0.5.0...v0.6.0) (2024-09-25)
### Features ### Features
* ui remake with tailwind ([22f823c](https://github.com/C4illin/ConvertX/commit/22f823c535b20382981f86a13616b830a1f3392f)) - ui remake with tailwind ([22f823c](https://github.com/C4illin/ConvertX/commit/22f823c535b20382981f86a13616b830a1f3392f))
### Bug Fixes ### Bug Fixes
* rename css file to force update cache, fixes [#141](https://github.com/C4illin/ConvertX/issues/141) ([47139a5](https://github.com/C4illin/ConvertX/commit/47139a550bd3d847da288c61bf8f88953b79c673)) - rename css file to force update cache, fixes [#141](https://github.com/C4illin/ConvertX/issues/141) ([47139a5](https://github.com/C4illin/ConvertX/commit/47139a550bd3d847da288c61bf8f88953b79c673))
## [0.5.0](https://github.com/C4illin/ConvertX/compare/v0.4.1...v0.5.0) (2024-09-20) ## [0.5.0](https://github.com/C4illin/ConvertX/compare/v0.4.1...v0.5.0) (2024-09-20)
### Features ### Features
* add option to customize how often files are automatically deleted ([317c932](https://github.com/C4illin/ConvertX/commit/317c932c2a26280bf37ed3d3bf9b879413590f5a)) - add option to customize how often files are automatically deleted ([317c932](https://github.com/C4illin/ConvertX/commit/317c932c2a26280bf37ed3d3bf9b879413590f5a))
### Bug Fixes ### Bug Fixes
* improve file name replacement logic ([60ba7c9](https://github.com/C4illin/ConvertX/commit/60ba7c93fbdc961f3569882fade7cc13dee7a7a5)) - improve file name replacement logic ([60ba7c9](https://github.com/C4illin/ConvertX/commit/60ba7c93fbdc961f3569882fade7cc13dee7a7a5))
## [0.4.1](https://github.com/C4illin/ConvertX/compare/v0.4.0...v0.4.1) (2024-09-15) ## [0.4.1](https://github.com/C4illin/ConvertX/compare/v0.4.0...v0.4.1) (2024-09-15)
### Bug Fixes ### Bug Fixes
* allow non lowercase true and false values, fixes [#122](https://github.com/C4illin/ConvertX/issues/122) ([bef1710](https://github.com/C4illin/ConvertX/commit/bef1710e3376baa7e25c107ded20a40d18b8c6b0)) - allow non lowercase true and false values, fixes [#122](https://github.com/C4illin/ConvertX/issues/122) ([bef1710](https://github.com/C4illin/ConvertX/commit/bef1710e3376baa7e25c107ded20a40d18b8c6b0))
## [0.4.0](https://github.com/C4illin/ConvertX/compare/v0.3.3...v0.4.0) (2024-08-26) ## [0.4.0](https://github.com/C4illin/ConvertX/compare/v0.3.3...v0.4.0) (2024-08-26)
### Features ### Features
* add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0)) - add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0))
* add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995)) - add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995))
* add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7)) - add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7))
* Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58)) - Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58))
### Bug Fixes ### Bug Fixes
* keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117)) - keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117))
* pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93)) - pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93))
* Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa)) - Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa))
## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30) ## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30)
### Bug Fixes ### Bug Fixes
* downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c)) - 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) ## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09)
### Bug Fixes ### 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) - 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) ## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27)
### Bug Fixes ### Bug Fixes
* release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf)) - 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) ## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27)
### Features ### Features
* add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44) - 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)) - 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)) - 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) ## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20)
### Features ### Features
* add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482)) - 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) - 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) ## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10)
### Bug Fixes ### Bug Fixes
* fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23) - 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) ## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30)
### Bug Fixes ### 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) - :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) ## 0.1.0 (2024-05-30)
### Features ### Features
* remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b)) - remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
### Miscellaneous Chores ### Miscellaneous Chores
* release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431)) - release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431))

317
README.md
View file

@ -1,158 +1,159 @@
![ConvertX](images/logo.png) ![ConvertX](images/logo.png)
# ConvertX # ConvertX
[![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml) [![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
[![ghcr.io Pulls](https://img.shields.io/badge/dynamic/json?logo=github&url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FC4illin%2FConvertX%2Fconvertx.json&query=%24.downloads&label=ghcr.io%20pulls&cacheSeconds=14400)](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) [![ghcr.io Pulls](https://img.shields.io/badge/dynamic/json?logo=github&url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FC4illin%2FConvertX%2Fconvertx.json&query=%24.downloads&label=ghcr.io%20pulls&cacheSeconds=14400)](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX)
[![Docker Pulls](https://img.shields.io/docker/pulls/c4illin/convertx?style=flat&logo=docker&label=dockerhub%20pulls&link=https%3A%2F%2Fhub.docker.com%2Frepository%2Fdocker%2Fc4illin%2Fconvertx%2Fgeneral)](https://hub.docker.com/r/c4illin/convertx) [![Docker Pulls](https://img.shields.io/docker/pulls/c4illin/convertx?style=flat&logo=docker&label=dockerhub%20pulls&link=https%3A%2F%2Fhub.docker.com%2Frepository%2Fdocker%2Fc4illin%2Fconvertx%2Fgeneral)](https://hub.docker.com/r/c4illin/convertx)
[![GitHub Release](https://img.shields.io/github/v/release/C4illin/ConvertX)](https://github.com/C4illin/ConvertX/pkgs/container/convertx) [![GitHub Release](https://img.shields.io/github/v/release/C4illin/ConvertX)](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
![GitHub commits since latest release](https://img.shields.io/github/commits-since/C4illin/ConvertX/latest) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/C4illin/ConvertX/latest)
![GitHub repo size](https://img.shields.io/github/repo-size/C4illin/ConvertX) ![GitHub repo size](https://img.shields.io/github/repo-size/C4illin/ConvertX)
![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=) ![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=)
<a href="https://trendshift.io/repositories/13818" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13818" alt="C4illin%2FConvertX | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/13818" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13818" alt="C4illin%2FConvertX | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<!-- ![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=) -->
<!-- ![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=) -->
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
## Features
## Features
- Convert files to different formats
- Process multiple files at once - Convert files to different formats
- Password protection - Process multiple files at once
- Multiple accounts - Password protection
- Multiple accounts
## Converters supported
## Converters supported
| Converter | Use case | Converts from | Converts to |
|------------------------------------------------------------------------------|---------------|---------------|-------------| | 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 | | [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 | | [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 | | [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 | | [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 |
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 | | [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 | | [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
| [dvisvgm](https://dvisvgm.de/) | Vector images | 4 | 2 | | [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 | | [dvisvgm](https://dvisvgm.de/) | Vector images | 4 | 2 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 | | [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 |
| [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 | | [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 |
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 | | [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 | | [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 |
| [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 | | [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 |
| [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 |
<!-- many ffmpeg fileformats are duplicates --> <!-- many ffmpeg fileformats are duplicates -->
Any missing converter? Open an issue or pull request! Any missing converter? Open an issue or pull request!
## Deployment ## Deployment
> [!WARNING] > [!WARNING]
> If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true > If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true
```yml ```yml
# docker-compose.yml # docker-compose.yml
services: services:
convertx: convertx:
image: ghcr.io/c4illin/convertx image: ghcr.io/c4illin/convertx
container_name: convertx container_name: convertx
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
volumes: volumes:
- ./data:/app/data - ./data:/app/data
``` ```
or or
```bash ```bash
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
``` ```
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account. Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose. If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
### Environment variables ### Environment variables
All are optional, JWT_SECRET is recommended to be set. All are optional, JWT_SECRET is recommended to be set.
| Name | Default | Description | | Name | Default | Description |
|---------------------------|---------|-------------| | ------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token | | JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token |
| ACCOUNT_REGISTRATION | false | Allow users to register accounts | | ACCOUNT_REGISTRATION | false | Allow users to register accounts |
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally | | HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally | | ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable | | AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
| WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" | | WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" |
| FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` | | FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` |
| HIDE_HISTORY | false | Hide the history page | | HIDE_HISTORY | false | Hide the history page |
### Docker images ### Docker images
There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use. There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use.
The image is available on [GitHub Container Registry](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) and [Docker Hub](https://hub.docker.com/r/c4illin/convertx). The image is available on [GitHub Container Registry](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) and [Docker Hub](https://hub.docker.com/r/c4illin/convertx).
| Image | What it is | | Image | What it is |
|-------|------------| | -------------------------------------- | -------------------------------- |
| `image: ghcr.io/c4illin/convertx` | The latest release on ghcr | | `image: ghcr.io/c4illin/convertx` | The latest release on ghcr |
| `image: ghcr.io/c4illin/convertx:main` | The latest commit on ghcr | | `image: ghcr.io/c4illin/convertx:main` | The latest commit on ghcr |
| `image: c4illin/convertx` | The latest release on docker hub | | `image: c4illin/convertx` | The latest release on docker hub |
| `image: c4illin/convertx:main` | The latest commit on docker hub | | `image: c4illin/convertx:main` | The latest commit on docker hub |
![Release image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=release+image&trim=) ![Release image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=release+image&trim=)
![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=) ![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=)
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
### Tutorial
### Tutorial
> [!NOTE]
> These are written by other people, and may be outdated, incorrect or wrong. > [!NOTE]
> These are written by other people, and may be outdated, incorrect or wrong.
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
Tutorial in chinese: <https://xzllll.com/24092901/>
Tutorial in chinese: <https://xzllll.com/24092901/>
## Screenshots
## Screenshots
![ConvertX Preview](images/preview.png)
![ConvertX Preview](images/preview.png)
## Development
## Development
0. Install [Bun](https://bun.sh/) and Git
1. Clone the repository 0. Install [Bun](https://bun.sh/) and Git
2. `bun install` 1. Clone the repository
3. `bun run dev` 2. `bun install`
3. `bun run dev`
Pull requests are welcome! See below and open issues for the list of todos.
Pull requests are welcome! See below and open issues for the list of todos.
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
## Todo
## Todo
- [ ] Add options for converters
- [ ] Add tests - [ ] Add options for converters
- [ ] Make errors logs visible from the web ui - [ ] Add tests
- [ ] Add more converters: - [ ] Make errors logs visible from the web ui
- [ ] [deark](https://github.com/jsummers/deark) - [ ] Add more converters:
- [ ] LibreOffice - [ ] [deark](https://github.com/jsummers/deark)
- [ ] LibreOffice
## Contributors
## Contributors
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/> <a href="https://github.com/C4illin/ConvertX/graphs/contributors">
</a> <img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
</a>
## Star History
## Star History
<a href="https://github.com/C4illin/ConvertX/stargazers">
<picture> <a href="https://github.com/C4illin/ConvertX/stargazers">
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date&theme=dark" /> <picture>
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" /> <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date&theme=dark" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" /> <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
</picture> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
</a> </picture>
</a>

View file

@ -10,10 +10,7 @@
"attributePosition": "auto" "attributePosition": "auto"
}, },
"files": { "files": {
"ignore": [ "ignore": ["**/node_modules/**", "**/pico.lime.min.css"]
"**/node_modules/**",
"**/pico.lime.min.css"
]
}, },
"organizeImports": { "organizeImports": {
"enabled": true "enabled": true
@ -72,4 +69,4 @@
"attributePosition": "auto" "attributePosition": "auto"
} }
} }
} }

View file

@ -1,19 +1,19 @@
services: services:
convertx: convertx:
build: build:
context: . context: .
# dockerfile: Debian.Dockerfile # dockerfile: Debian.Dockerfile
volumes: volumes:
- ./data:/app/data - ./data:/app/data
environment: # Defaults are listed below. All are optional. environment: # Defaults are listed below. All are optional.
- ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account) - ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally - 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 - ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
- AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable - AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
# - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg # - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg
# - WEBROOT=/convertx # the root path of the web interface, leave empty to disable # - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
# - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false # - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false
- TZ=Europe/Stockholm # set your timezone, defaults to UTC - TZ=Europe/Stockholm # set your timezone, defaults to UTC
ports: ports:
- 3000:3000 - 3000:3000

View file

@ -1,66 +1,66 @@
import js from "@eslint/js"; import js from "@eslint/js";
import eslintParserTypeScript from "@typescript-eslint/parser"; import eslintParserTypeScript from "@typescript-eslint/parser";
import type { Linter } from "eslint"; import type { Linter } from "eslint";
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss"; import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort"; import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
import globals from "globals"; import globals from "globals";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
export default [ export default [
js.configs.recommended, js.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
// ...tailwind.configs["flat/recommended"], // ...tailwind.configs["flat/recommended"],
{ {
plugins: { plugins: {
"simple-import-sort": simpleImportSortPlugin, "simple-import-sort": simpleImportSortPlugin,
"better-tailwindcss": eslintPluginBetterTailwindcss, "better-tailwindcss": eslintPluginBetterTailwindcss,
}, },
ignores: ["**/node_modules/**"], ignores: ["**/node_modules/**"],
languageOptions: { languageOptions: {
parser: eslintParserTypeScript, parser: eslintParserTypeScript,
parserOptions: { parserOptions: {
project: true, project: true,
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
}, },
}, },
globals: { globals: {
...globals.node, ...globals.node,
...globals.browser, ...globals.browser,
}, },
}, },
files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"], files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"],
settings: { settings: {
"better-tailwindcss": { "better-tailwindcss": {
entryPoint: "src/main.css", entryPoint: "src/main.css",
}, },
}, },
rules: { rules: {
...eslintPluginBetterTailwindcss.configs["recommended-warn"]?.rules, ...(eslintPluginBetterTailwindcss.configs["recommended-warn"] ?? {}).rules,
...eslintPluginBetterTailwindcss.configs["stylistic-warn"]?.rules, ...(eslintPluginBetterTailwindcss.configs["stylistic-warn"] ?? {}).rules,
// "tailwindcss/classnames-order": "off", // "tailwindcss/classnames-order": "off",
"better-tailwindcss/multiline": [ "better-tailwindcss/multiline": [
"warn", "warn",
{ {
group: "newLine", group: "newLine",
printWidth: 100, printWidth: 100,
}, },
], ],
"better-tailwindcss/no-unregistered-classes": [ "better-tailwindcss/no-unregistered-classes": [
"warn", "warn",
{ {
ignore: [ ignore: [
"^group(?:\\/(\\S*))?$", "^group(?:\\/(\\S*))?$",
"^peer(?:\\/(\\S*))?$", "^peer(?:\\/(\\S*))?$",
"select_container", "select_container",
"convert_to_popup", "convert_to_popup",
"convert_to_group", "convert_to_group",
"target", "target",
"convert_to_target", "convert_to_target",
"job-details-toggle", "job-details-toggle",
], ],
}, },
], ],
}, },
}, },
] as Linter.Config[]; ] as Linter.Config[];

View file

@ -4,12 +4,15 @@
"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": "eslint --fix .", "format": "run-p 'format:*'",
"format:eslint": "eslint --fix .",
"format:prettier": "prettier --write .",
"build": "bunx @tailwindcss/cli -i ./src/main.css -o ./public/generated.css", "build": "bunx @tailwindcss/cli -i ./src/main.css -o ./public/generated.css",
"lint": "run-p 'lint:*'", "lint": "run-p 'lint:*'",
"lint:tsc": "tsc --noEmit", "lint:tsc": "tsc --noEmit",
"lint:knip": "knip", "lint:knip": "knip",
"lint:eslint": "eslint ." "lint:eslint": "eslint .",
"lint:prettier": "prettier --check ."
}, },
"dependencies": { "dependencies": {
"@elysiajs/html": "^1.3.0", "@elysiajs/html": "^1.3.0",

View file

@ -1,5 +1,5 @@
export default { export default {
plugins: { plugins: {
"@tailwindcss/postcss": {}, "@tailwindcss/postcss": {},
}, },
}; };

View file

@ -3,7 +3,7 @@
*/ */
const config = { const config = {
arrowParens: "always", arrowParens: "always",
printWidth: 80, printWidth: 100,
singleQuote: false, singleQuote: false,
semi: true, semi: true,
tabWidth: 2, tabWidth: 2,

View file

@ -72,9 +72,7 @@ function handleFile(file) {
const selectContainer = document.querySelector("form .select_container"); const selectContainer = document.querySelector("form .select_container");
const updateSearchBar = () => { const updateSearchBar = () => {
const convertToInput = document.querySelector( const convertToInput = document.querySelector("input[name='convert_to_search']");
"input[name='convert_to_search']",
);
const convertToPopup = document.querySelector(".convert_to_popup"); const convertToPopup = document.querySelector(".convert_to_popup");
const convertToGroupElements = document.querySelectorAll(".convert_to_group"); const convertToGroupElements = document.querySelectorAll(".convert_to_group");
const convertToGroups = {}; const convertToGroups = {};
@ -195,8 +193,7 @@ const deleteRow = (target) => {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}) }).catch((err) => console.log(err));
.catch((err) => console.log(err));
}; };
const uploadFile = (file) => { const uploadFile = (file) => {
@ -234,7 +231,7 @@ const uploadFile = (file) => {
console.log(`upload progress (${file.name}):`, (100 * sent) / total); console.log(`upload progress (${file.name}):`, (100 * sent) / total);
let progressbar = file.htmlRow.getElementsByTagName("progress"); let progressbar = file.htmlRow.getElementsByTagName("progress");
progressbar[0].value = ((100 * sent) / total); progressbar[0].value = (100 * sent) / total;
}; };
xhr.onerror = (e) => { xhr.onerror = (e) => {

View file

@ -1,11 +1,8 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:recommended", ":disableDependencyDashboard"],
"config:recommended",
":disableDependencyDashboard"
],
"lockFileMaintenance": { "lockFileMaintenance": {
"enabled": true, "enabled": true,
"automerge": true "automerge": true
} }
} }

2
reset.d.ts vendored
View file

@ -1 +1 @@
import "@total-typescript/ts-reset"; import "@total-typescript/ts-reset";

View file

@ -17,23 +17,9 @@ export const BaseHtml = ({
<meta name="webroot" content={webroot} /> <meta name="webroot" content={webroot} />
<title safe>{title}</title> <title safe>{title}</title>
<link rel="stylesheet" href={`${webroot}/generated.css`} /> <link rel="stylesheet" href={`${webroot}/generated.css`} />
<link <link rel="apple-touch-icon" sizes="180x180" href={`${webroot}/apple-touch-icon.png`} />
rel="apple-touch-icon" <link rel="icon" type="image/png" sizes="32x32" href={`${webroot}/favicon-32x32.png`} />
sizes="180x180" <link rel="icon" type="image/png" sizes="16x16" href={`${webroot}/favicon-16x16.png`} />
href={`${webroot}/apple-touch-icon.png`}
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href={`${webroot}/favicon-32x32.png`}
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href={`${webroot}/favicon-16x16.png`}
/>
<link rel="manifest" href={`${webroot}/site.webmanifest`} /> <link rel="manifest" href={`${webroot}/site.webmanifest`} />
</head> </head>
<body class="flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200"> <body class="flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200">

View file

@ -1,139 +1,139 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
export const properties = { export const properties = {
from: { from: {
object: [ object: [
"3d", "3d",
"3ds", "3ds",
"3mf", "3mf",
"ac", "ac",
"ac3d", "ac3d",
"acc", "acc",
"amf", "amf",
"amj", "amj",
"ase", "ase",
"ask", "ask",
"assbin", "assbin",
"b3d", "b3d",
"blend", "blend",
"bsp", "bsp",
"bvh", "bvh",
"cob", "cob",
"csm", "csm",
"dae", "dae",
"dxf", "dxf",
"enff", "enff",
"fbx", "fbx",
"glb", "glb",
"gltf", "gltf",
"hmb", "hmb",
"hmp", "hmp",
"ifc", "ifc",
"ifczip", "ifczip",
"iqm", "iqm",
"irr", "irr",
"irrmesh", "irrmesh",
"lwo", "lwo",
"lws", "lws",
"lxo", "lxo",
"m3d", "m3d",
"md2", "md2",
"md3", "md3",
"md5anim", "md5anim",
"md5camera", "md5camera",
"md5mesh", "md5mesh",
"mdc", "mdc",
"mdl", "mdl",
"mesh.xml", "mesh.xml",
"mesh", "mesh",
"mot", "mot",
"ms3d", "ms3d",
"ndo", "ndo",
"nff", "nff",
"obj", "obj",
"off", "off",
"ogex", "ogex",
"pk3", "pk3",
"ply", "ply",
"pmx", "pmx",
"prj", "prj",
"q3o", "q3o",
"q3s", "q3s",
"raw", "raw",
"scn", "scn",
"sib", "sib",
"smd", "smd",
"step", "step",
"stl", "stl",
"stp", "stp",
"ter", "ter",
"uc", "uc",
"usd", "usd",
"usda", "usda",
"usdc", "usdc",
"usdz", "usdz",
"vta", "vta",
"x", "x",
"x3d", "x3d",
"x3db", "x3db",
"xgl", "xgl",
"xml", "xml",
"zae", "zae",
"zgl", "zgl",
], ],
}, },
to: { to: {
object: [ object: [
"3ds", "3ds",
"3mf", "3mf",
"assbin", "assbin",
"assjson", "assjson",
"assxml", "assxml",
"collada", "collada",
"dae", "dae",
"fbx", "fbx",
"fbxa", "fbxa",
"glb", "glb",
"glb2", "glb2",
"gltf", "gltf",
"gltf2", "gltf2",
"json", "json",
"obj", "obj",
"objnomtl", "objnomtl",
"pbrt", "pbrt",
"ply", "ply",
"plyb", "plyb",
"stl", "stl",
"stlb", "stlb",
"stp", "stp",
"x", "x",
], ],
}, },
}; };
export async function convert( export async function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => { execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);
} }
if (stdout) { if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("Done"); resolve("Done");
}); });
}); });
} }

View file

@ -29,24 +29,20 @@ export function convert(
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile( execFile("dvisvgm", [...inputArgs, filePath, "-o", targetPath], (error, stdout, stderr) => {
"dvisvgm", if (error) {
[...inputArgs, filePath, "-o", targetPath], reject(`error: ${error}`);
(error, stdout, stderr) => { }
if (error) {
reject(`error: ${error}`);
}
if (stdout) { if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("Done"); resolve("Done");
}, });
);
}); });
} }

File diff suppressed because it is too large Load diff

View file

@ -1,340 +1,336 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
export const properties = { export const properties = {
from: { from: {
image: [ image: [
"3fr", "3fr",
"8bim", "8bim",
"8bimtext", "8bimtext",
"8bimwtext", "8bimwtext",
"app1", "app1",
"app1jpeg", "app1jpeg",
"art", "art",
"arw", "arw",
"avs", "avs",
"b", "b",
"bie", "bie",
"bigtiff", "bigtiff",
"bmp", "bmp",
"c", "c",
"cals", "cals",
"caption", "caption",
"cin", "cin",
"cmyk", "cmyk",
"cmyka", "cmyka",
"cr2", "cr2",
"crw", "crw",
"cur", "cur",
"cut", "cut",
"dcm", "dcm",
"dcr", "dcr",
"dcx", "dcx",
"dng", "dng",
"dpx", "dpx",
"epdf", "epdf",
"epi", "epi",
"eps", "eps",
"epsf", "epsf",
"epsi", "epsi",
"ept", "ept",
"ept2", "ept2",
"ept3", "ept3",
"erf", "erf",
"exif", "exif",
"fax", "fax",
"file", "file",
"fits", "fits",
"fractal", "fractal",
"ftp", "ftp",
"g", "g",
"gif", "gif",
"gif87", "gif87",
"gradient", "gradient",
"gray", "gray",
"graya", "graya",
"heic", "heic",
"heif", "heif",
"hrz", "hrz",
"http", "http",
"icb", "icb",
"icc", "icc",
"icm", "icm",
"ico", "ico",
"icon", "icon",
"identity", "identity",
"image", "image",
"iptc", "iptc",
"iptctext", "iptctext",
"iptcwtext", "iptcwtext",
"jbg", "jbg",
"jbig", "jbig",
"jng", "jng",
"jnx", "jnx",
"jpeg", "jpeg",
"jpg", "jpg",
"k", "k",
"k25", "k25",
"kdc", "kdc",
"label", "label",
"m", "m",
"mac", "mac",
"map", "map",
"mat", "mat",
"mef", "mef",
"miff", "miff",
"mng", "mng",
"mono", "mono",
"mpc", "mpc",
"mrw", "mrw",
"msl", "msl",
"mtv", "mtv",
"mvg", "mvg",
"nef", "nef",
"null", "null",
"o", "o",
"orf", "orf",
"otb", "otb",
"p7", "p7",
"pal", "pal",
"palm", "palm",
"pam", "pam",
"pbm", "pbm",
"pcd", "pcd",
"pcds", "pcds",
"pct", "pct",
"pcx", "pcx",
"pdb", "pdb",
"pdf", "pdf",
"pef", "pef",
"pfa", "pfa",
"pfb", "pfb",
"pgm", "pgm",
"picon", "picon",
"pict", "pict",
"pix", "pix",
"plasma", "plasma",
"png", "png",
"png00", "png00",
"png24", "png24",
"png32", "png32",
"png48", "png48",
"png64", "png64",
"png8", "png8",
"pnm", "pnm",
"ppm", "ppm",
"ps", "ps",
"ptif", "ptif",
"pwp", "pwp",
"r", "r",
"raf", "raf",
"ras", "ras",
"rgb", "rgb",
"rgba", "rgba",
"rla", "rla",
"rle", "rle",
"sct", "sct",
"sfw", "sfw",
"sgi", "sgi",
"sr2", "sr2",
"srf", "srf",
"stegano", "stegano",
"sun", "sun",
"svg", "svg",
"svgz", "svgz",
"text", "text",
"tga", "tga",
"tif", "tif",
"tiff", "tiff",
"tile", "tile",
"tim", "tim",
"topol", "topol",
"ttf", "ttf",
"txt", "txt",
"uyvy", "uyvy",
"vda", "vda",
"vicar", "vicar",
"vid", "vid",
"viff", "viff",
"vst", "vst",
"wbmp", "wbmp",
"webp", "webp",
"wmf", "wmf",
"wpg", "wpg",
"x3f", "x3f",
"xbm", "xbm",
"xc", "xc",
"xcf", "xcf",
"xmp", "xmp",
"xpm", "xpm",
"xv", "xv",
"xwd", "xwd",
"y", "y",
"yuv", "yuv",
], ],
}, },
to: { to: {
image: [ image: [
"8bim", "8bim",
"8bimtext", "8bimtext",
"8bimwtext", "8bimwtext",
"app1", "app1",
"app1jpeg", "app1jpeg",
"art", "art",
"avs", "avs",
"b", "b",
"bie", "bie",
"bigtiff", "bigtiff",
"bmp", "bmp",
"bmp2", "bmp2",
"bmp3", "bmp3",
"brf", "brf",
"c", "c",
"cals", "cals",
"cin", "cin",
"cmyk", "cmyk",
"cmyka", "cmyka",
"dcx", "dcx",
"dpx", "dpx",
"epdf", "epdf",
"epi", "epi",
"eps", "eps",
"eps2", "eps2",
"eps3", "eps3",
"epsf", "epsf",
"epsi", "epsi",
"ept", "ept",
"ept2", "ept2",
"ept3", "ept3",
"exif", "exif",
"fax", "fax",
"fits", "fits",
"g", "g",
"gif", "gif",
"gif87", "gif87",
"gray", "gray",
"graya", "graya",
"histogram", "histogram",
"html", "html",
"icb", "icb",
"icc", "icc",
"icm", "icm",
"info", "info",
"iptc", "iptc",
"iptctext", "iptctext",
"iptcwtext", "iptcwtext",
"isobrl", "isobrl",
"isobrl6", "isobrl6",
"jbg", "jbg",
"jbig", "jbig",
"jng", "jng",
"jpeg", "jpeg",
"k", "k",
"m", "m",
"m2v", "m2v",
"map", "map",
"mat", "mat",
"matte", "matte",
"miff", "miff",
"mng", "mng",
"mono", "mono",
"mpc", "mpc",
"mpeg", "mpeg",
"mpg", "mpg",
"msl", "msl",
"mtv", "mtv",
"mvg", "mvg",
"null", "null",
"o", "o",
"otb", "otb",
"p7", "p7",
"pal", "pal",
"pam", "pam",
"pbm", "pbm",
"pcd", "pcd",
"pcds", "pcds",
"pcl", "pcl",
"pct", "pct",
"pcx", "pcx",
"pdb", "pdb",
"pdf", "pdf",
"pgm", "pgm",
"picon", "picon",
"pict", "pict",
"png", "png",
"png00", "png00",
"png24", "png24",
"png32", "png32",
"png48", "png48",
"png64", "png64",
"png8", "png8",
"pnm", "pnm",
"ppm", "ppm",
"preview", "preview",
"ps", "ps",
"ps2", "ps2",
"ps3", "ps3",
"ptif", "ptif",
"r", "r",
"ras", "ras",
"rgb", "rgb",
"rgba", "rgba",
"sgi", "sgi",
"shtml", "shtml",
"sun", "sun",
"text", "text",
"tga", "tga",
"tiff", "tiff",
"txt", "txt",
"ubrl", "ubrl",
"ubrl6", "ubrl6",
"uil", "uil",
"uyvy", "uyvy",
"vda", "vda",
"vicar", "vicar",
"vid", "vid",
"viff", "viff",
"vst", "vst",
"wbmp", "wbmp",
"webp", "webp",
"x", "x",
"xbm", "xbm",
"xmp", "xmp",
"xpm", "xpm",
"xv", "xv",
"xwd", "xwd",
"y", "y",
"yuv", "yuv",
], ],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile( execFile("gm", ["convert", filePath, targetPath], (error, stdout, stderr) => {
"gm", if (error) {
["convert", filePath, targetPath], reject(`error: ${error}`);
(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("Done");
} });
});
resolve("Done"); }
},
);
});
}

View file

@ -452,12 +452,7 @@ export function convert(
let inputArgs: string[] = []; let inputArgs: string[] = [];
if (convertTo === "ico") { if (convertTo === "ico") {
outputArgs = [ outputArgs = ["-define", "icon:auto-resize=256,128,64,48,32,16", "-background", "none"];
"-define",
"icon:auto-resize=256,128,64,48,32,16",
"-background",
"none",
];
if (fileType === "svg") { if (fileType === "svg") {
// this might be a bit too much, but it works // this might be a bit too much, but it works

View file

@ -36,24 +36,20 @@ export function convert(
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile( execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => {
"inkscape", if (error) {
[filePath, "-o", targetPath], reject(`error: ${error}`);
(error, stdout, stderr) => { }
if (error) {
reject(`error: ${error}`);
}
if (stdout) { if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("Done"); resolve("Done");
}, });
);
}); });
} }

View file

@ -2,19 +2,7 @@ import { execFile } from "child_process";
export const properties = { export const properties = {
from: { from: {
images: [ images: ["avci", "avcs", "avif", "h264", "heic", "heics", "heif", "heifs", "hif", "mkv", "mp4"],
"avci",
"avcs",
"avif",
"h264",
"heic",
"heics",
"heif",
"heifs",
"hif",
"mkv",
"mp4",
],
}, },
to: { to: {
images: ["jpeg", "png", "y4m"], images: ["jpeg", "png", "y4m"],
@ -30,24 +18,20 @@ export function convert(
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile( execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => {
"heif-convert", if (error) {
[filePath, targetPath], reject(`error: ${error}`);
(error, stdout, stderr) => { }
if (error) {
reject(`error: ${error}`);
}
if (stdout) { if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("Done"); resolve("Done");
}, });
);
}); });
} }

View file

@ -1,71 +1,49 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
// declare possible conversions // declare possible conversions
export const properties = { export const properties = {
from: { from: {
jxl: ["jxl"], jxl: ["jxl"],
images: [ images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
"apng", },
"exr", to: {
"gif", jxl: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
"jpeg", images: ["jxl"],
"pam", },
"pfm", };
"pgm",
"pgx", export function convert(
"png", filePath: string,
"ppm", fileType: string,
], convertTo: string,
}, targetPath: string,
to: { // eslint-disable-next-line @typescript-eslint/no-unused-vars
jxl: [ options?: unknown,
"apng", ): Promise<string> {
"exr", let tool = "";
"gif", if (fileType === "jxl") {
"jpeg", tool = "djxl";
"pam", }
"pfm",
"pgm", if (convertTo === "jxl") {
"pgx", tool = "cjxl";
"png", }
"ppm",
], return new Promise((resolve, reject) => {
images: ["jxl"], execFile(tool, [filePath, targetPath], (error, stdout, stderr) => {
}, if (error) {
}; reject(`error: ${error}`);
}
export function convert(
filePath: string, if (stdout) {
fileType: string, console.log(`stdout: ${stdout}`);
convertTo: string, }
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars if (stderr) {
options?: unknown, console.error(`stderr: ${stderr}`);
): Promise<string> { }
let tool = "";
if (fileType === "jxl") { resolve("Done");
tool = "djxl"; });
} });
}
if (convertTo === "jxl") {
tool = "cjxl";
}
return new Promise((resolve, reject) => {
execFile(tool, [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View file

@ -1,19 +1,21 @@
import { normalizeFiletype } from "../helpers/normalizeFiletype"; import { normalizeFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp"; import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg"; import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick"; import {
convert as convertGraphicsmagick,
properties as propertiesGraphicsmagick,
} from "./graphicsmagick";
import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick";
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape"; import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl"; import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc"; import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace";
import { convert as convertresvg, properties as propertiesresvg } from "./resvg"; import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
import { convert as convertImage, properties as propertiesImage } from "./vips"; import { convert as convertImage, properties as propertiesImage } from "./vips";
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex"; import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace";
import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick";
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular // This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
@ -113,7 +115,7 @@ export async function mainConverter(
) { ) {
const fileType = normalizeFiletype(fileTypeOriginal); const fileType = normalizeFiletype(fileTypeOriginal);
let converterFunc: typeof properties["libjxl"]["converter"] | undefined; let converterFunc: (typeof properties)["libjxl"]["converter"] | undefined;
if (converterName) { if (converterName) {
converterFunc = properties[converterName]?.converter; converterFunc = properties[converterName]?.converter;
@ -139,20 +141,12 @@ export async function mainConverter(
} }
if (!converterFunc) { if (!converterFunc) {
console.log( console.log(`No available converter supports converting from ${fileType} to ${convertTo}.`);
`No available converter supports converting from ${fileType} to ${convertTo}.`,
);
return "File type not supported"; return "File type not supported";
} }
try { try {
const result = await converterFunc( const result = await converterFunc(inputFilePath, fileType, convertTo, targetPath, options);
inputFilePath,
fileType,
convertTo,
targetPath,
options,
);
console.log( console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`, `Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`,
@ -192,8 +186,7 @@ for (const converterName in properties) {
possibleTargets[extension] = {}; possibleTargets[extension] = {};
} }
possibleTargets[extension][converterName] = possibleTargets[extension][converterName] = converterProperties.to[key] || [];
converterProperties.to[key] || [];
} }
} }
} }
@ -294,4 +287,4 @@ export const getAllInputs = (converter: string) => {
// } // }
// // print the number of unique Inputs and Outputs // // print the number of unique Inputs and Outputs
// console.log(`Unique Formats: ${uniqueFormats.size}`); // console.log(`Unique Formats: ${uniqueFormats.size}`);

View file

@ -1,162 +1,162 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
export const properties = { export const properties = {
from: { from: {
text: [ text: [
"textile", "textile",
"tikiwiki", "tikiwiki",
"tsv", "tsv",
"twiki", "twiki",
"typst", "typst",
"vimwiki", "vimwiki",
"biblatex", "biblatex",
"bibtex", "bibtex",
"bits", "bits",
"commonmark", "commonmark",
"commonmark_x", "commonmark_x",
"creole", "creole",
"csljson", "csljson",
"csv", "csv",
"djot", "djot",
"docbook", "docbook",
"docx", "docx",
"dokuwiki", "dokuwiki",
"endnotexml", "endnotexml",
"epub", "epub",
"fb2", "fb2",
"gfm", "gfm",
"haddock", "haddock",
"html", "html",
"ipynb", "ipynb",
"jats", "jats",
"jira", "jira",
"json", "json",
"latex", "latex",
"man", "man",
"markdown", "markdown",
"markdown_mmd", "markdown_mmd",
"markdown_phpextra", "markdown_phpextra",
"markdown_strict", "markdown_strict",
"mediawiki", "mediawiki",
"muse", "muse",
"pandoc native", "pandoc native",
"opml", "opml",
"org", "org",
"ris", "ris",
"rst", "rst",
"rtf", "rtf",
"t2t", "t2t",
], ],
}, },
to: { to: {
text: [ text: [
"tei", "tei",
"texinfo", "texinfo",
"textile", "textile",
"typst", "typst",
"xwiki", "xwiki",
"zimwiki", "zimwiki",
"asciidoc", "asciidoc",
"asciidoc_legacy", "asciidoc_legacy",
"asciidoctor", "asciidoctor",
"beamer", "beamer",
"biblatex", "biblatex",
"bibtex", "bibtex",
"chunkedhtml", "chunkedhtml",
"commonmark", "commonmark",
"commonmark_x", "commonmark_x",
"context", "context",
"csljson", "csljson",
"djot", "djot",
"docbook", "docbook",
"docbook4", "docbook4",
"docbook5", "docbook5",
"docx", "docx",
"dokuwiki", "dokuwiki",
"dzslides", "dzslides",
"epub", "epub",
"epub2", "epub2",
"epub3", "epub3",
"fb2", "fb2",
"gfm", "gfm",
"haddock", "haddock",
"html", "html",
"html4", "html4",
"html5", "html5",
"icml", "icml",
"ipynb", "ipynb",
"jats", "jats",
"jats_archiving", "jats_archiving",
"jats_articleauthoring", "jats_articleauthoring",
"jats_publishing", "jats_publishing",
"jira", "jira",
"json", "json",
"latex", "latex",
"man", "man",
"markdown", "markdown",
"markdown_mmd", "markdown_mmd",
"markdown_phpextra", "markdown_phpextra",
"markdown_strict", "markdown_strict",
"markua", "markua",
"mediawiki", "mediawiki",
"ms", "ms",
"muse", "muse",
"pandoc native", "pandoc native",
"odt", "odt",
"opendocument", "opendocument",
"opml", "opml",
"org", "org",
"pdf", "pdf",
"plain", "plain",
"pptx", "pptx",
"revealjs", "revealjs",
"rst", "rst",
"rtf", "rtf",
"s5", "s5",
"slideous", "slideous",
"slidy", "slidy",
], ],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
// set xelatex here // set xelatex here
const xelatex = ["pdf", "latex"]; const xelatex = ["pdf", "latex"];
// Build arguments array // Build arguments array
const args: string[] = []; const args: string[] = [];
if (xelatex.includes(convertTo)) { if (xelatex.includes(convertTo)) {
args.push("--pdf-engine=xelatex"); args.push("--pdf-engine=xelatex");
} }
args.push(filePath); args.push(filePath);
args.push("-f", fileType); args.push("-f", fileType);
args.push("-t", convertTo); args.push("-t", convertTo);
args.push("-o", targetPath); args.push("-o", targetPath);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("pandoc", args, (error, stdout, stderr) => { execFile("pandoc", args, (error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);
} }
if (stdout) { if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("Done"); resolve("Done");
}); });
}); });
} }

View file

@ -5,33 +5,45 @@ export const properties = {
images: ["pnm", "pbm", "pgm", "bmp"], images: ["pnm", "pbm", "pgm", "bmp"],
}, },
to: { to: {
images: ["svg", "pdf", "pdfpage", "eps", "postscript", "ps", "dxf", "geojson", "pgm", "gimppath", "xfig"], images: [
"svg",
"pdf",
"pdfpage",
"eps",
"postscript",
"ps",
"dxf",
"geojson",
"pgm",
"gimppath",
"xfig",
],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => { execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);
} }
if (stdout) { if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("Done"); resolve("Done");
});
}); });
} });
}

View file

@ -1,37 +1,37 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
export const properties = { export const properties = {
from: { from: {
images: ["svg"], images: ["svg"],
}, },
to: { to: {
images: ["png"], images: ["png"],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => { execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);
} }
if (stdout) { if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("Done"); resolve("Done");
}); });
}); });
} }

View file

@ -1,142 +1,138 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
// declare possible conversions // declare possible conversions
export const properties = { export const properties = {
from: { from: {
images: [ images: [
"avif", "avif",
"bif", "bif",
"csv", "csv",
"exr", "exr",
"fits", "fits",
"gif", "gif",
"hdr.gz", "hdr.gz",
"hdr", "hdr",
"heic", "heic",
"heif", "heif",
"img.gz", "img.gz",
"img", "img",
"j2c", "j2c",
"j2k", "j2k",
"jp2", "jp2",
"jpeg", "jpeg",
"jpx", "jpx",
"jxl", "jxl",
"mat", "mat",
"mrxs", "mrxs",
"ndpi", "ndpi",
"nia.gz", "nia.gz",
"nia", "nia",
"nii.gz", "nii.gz",
"nii", "nii",
"pdf", "pdf",
"pfm", "pfm",
"pgm", "pgm",
"pic", "pic",
"png", "png",
"ppm", "ppm",
"raw", "raw",
"scn", "scn",
"svg", "svg",
"svs", "svs",
"svslide", "svslide",
"szi", "szi",
"tif", "tif",
"tiff", "tiff",
"v", "v",
"vips", "vips",
"vms", "vms",
"vmu", "vmu",
"webp", "webp",
"zip", "zip",
], ],
}, },
to: { to: {
images: [ images: [
"avif", "avif",
"dzi", "dzi",
"fits", "fits",
"gif", "gif",
"hdr.gz", "hdr.gz",
"heic", "heic",
"heif", "heif",
"img.gz", "img.gz",
"j2c", "j2c",
"j2k", "j2k",
"jp2", "jp2",
"jpeg", "jpeg",
"jpx", "jpx",
"jxl", "jxl",
"mat", "mat",
"nia.gz", "nia.gz",
"nia", "nia",
"nii.gz", "nii.gz",
"nii", "nii",
"png", "png",
"tiff", "tiff",
"vips", "vips",
"webp", "webp",
], ],
}, },
options: { options: {
svg: { svg: {
scale: { scale: {
description: "Scale the image up or down", description: "Scale the image up or down",
type: "number", type: "number",
default: 1, default: 1,
}, },
}, },
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
// if (fileType === "svg") { // if (fileType === "svg") {
// const scale = options.scale || 1; // const scale = options.scale || 1;
// const metadata = await sharp(filePath).metadata(); // const metadata = await sharp(filePath).metadata();
// if (!metadata || !metadata.width || !metadata.height) { // if (!metadata || !metadata.width || !metadata.height) {
// throw new Error("Could not get metadata from image"); // throw new Error("Could not get metadata from image");
// } // }
// const newWidth = Math.round(metadata.width * scale); // const newWidth = Math.round(metadata.width * scale);
// const newHeight = Math.round(metadata.height * scale); // const newHeight = Math.round(metadata.height * scale);
// return await sharp(filePath) // return await sharp(filePath)
// .resize(newWidth, newHeight) // .resize(newWidth, newHeight)
// .toFormat(convertTo) // .toFormat(convertTo)
// .toFile(targetPath); // .toFile(targetPath);
// } // }
let action = "copy"; let action = "copy";
if (fileType === "pdf") { if (fileType === "pdf") {
action = "pdfload"; action = "pdfload";
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile( execFile("vips", [action, filePath, targetPath], (error, stdout, stderr) => {
"vips", if (error) {
[action, filePath, targetPath], reject(`error: ${error}`);
(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("Done");
} });
});
resolve("Done"); }
},
);
});
}

View file

@ -1,53 +1,44 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
export const properties = { export const properties = {
from: { from: {
text: ["tex", "latex"], text: ["tex", "latex"],
}, },
to: { to: {
text: ["pdf"], text: ["pdf"],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "") // const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
const outputPath = targetPath const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "");
.split("/")
.slice(0, -1) execFile(
.join("/") "latexmk",
.replace("./", ""); ["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath],
(error, stdout, stderr) => {
execFile( if (error) {
"latexmk", reject(`error: ${error}`);
[ }
"-xelatex",
"-interaction=nonstopmode", if (stdout) {
`-output-directory=${outputPath}`, console.log(`stdout: ${stdout}`);
filePath, }
],
(error, stdout, stderr) => { if (stderr) {
if (error) { console.error(`stderr: ${stderr}`);
reject(`error: ${error}`); }
}
resolve("Done");
if (stdout) { },
console.log(`stdout: ${stdout}`); );
} });
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
});
}

View file

@ -1,4 +1,5 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
const db = new Database("./data/mydb.sqlite", { create: true }); const db = new Database("./data/mydb.sqlite", { create: true });
if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) { if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
@ -27,13 +28,9 @@ CREATE TABLE IF NOT EXISTS jobs (
PRAGMA user_version = 1;`); PRAGMA user_version = 1;`);
} }
const dbVersion = ( const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
db.query("PRAGMA user_version").get() as { user_version?: number }
).user_version;
if (dbVersion === 0) { if (dbVersion === 0) {
db.exec( db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
"ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';",
);
db.exec("PRAGMA user_version = 1;"); db.exec("PRAGMA user_version = 1;");
console.log("Updated database to version 1."); console.log("Updated database to version 1.");
} }
@ -41,4 +38,4 @@ if (dbVersion === 0) {
// enable WAL mode // enable WAL mode
db.exec("PRAGMA journal_mode = WAL;"); db.exec("PRAGMA journal_mode = WAL;");
export default db; export default db;

View file

@ -20,4 +20,4 @@ export class User {
id!: number; id!: number;
email!: string; email!: string;
password!: string; password!: string;
} }

View file

@ -1,8 +1,7 @@
export const ACCOUNT_REGISTRATION = export const ACCOUNT_REGISTRATION =
process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false; process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false;
export const HTTP_ALLOWED = export const HTTP_ALLOWED = process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
export const ALLOW_UNAUTHENTICATED = export const ALLOW_UNAUTHENTICATED =
process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false; process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
@ -11,7 +10,6 @@ export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS) ? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
: 24; : 24;
export const HIDE_HISTORY = export const HIDE_HISTORY = process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
export const WEBROOT = process.env.WEBROOT ?? ""; export const WEBROOT = process.env.WEBROOT ?? "";

View file

@ -1,37 +1,37 @@
export const normalizeFiletype = (filetype: string): string => { export const normalizeFiletype = (filetype: string): string => {
const lowercaseFiletype = filetype.toLowerCase(); const lowercaseFiletype = filetype.toLowerCase();
switch (lowercaseFiletype) { switch (lowercaseFiletype) {
case "jfif": case "jfif":
case "jpg": case "jpg":
return "jpeg"; return "jpeg";
case "htm": case "htm":
return "html"; return "html";
case "tex": case "tex":
return "latex"; return "latex";
case "md": case "md":
return "markdown"; return "markdown";
case "unknown": case "unknown":
return "m4a"; return "m4a";
default: default:
return lowercaseFiletype; return lowercaseFiletype;
} }
}; };
export const normalizeOutputFiletype = (filetype: string): string => { export const normalizeOutputFiletype = (filetype: string): string => {
const lowercaseFiletype = filetype.toLowerCase(); const lowercaseFiletype = filetype.toLowerCase();
switch (lowercaseFiletype) { switch (lowercaseFiletype) {
case "jpeg": case "jpeg":
return "jpg"; return "jpg";
case "latex": case "latex":
return "tex"; return "tex";
case "markdown_phpextra": case "markdown_phpextra":
case "markdown_strict": case "markdown_strict":
case "markdown_mmd": case "markdown_mmd":
case "markdown": case "markdown":
return "md"; return "md";
default: default:
return lowercaseFiletype; return lowercaseFiletype;
} }
}; };

View file

@ -2,22 +2,21 @@ import { rmSync } from "node:fs";
import { mkdir } from "node:fs/promises"; import { mkdir } from "node:fs/promises";
import { html } from "@elysiajs/html"; import { html } from "@elysiajs/html";
import { staticPlugin } from "@elysiajs/static"; import { staticPlugin } from "@elysiajs/static";
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import "./helpers/printVersions"; import "./helpers/printVersions";
import { AUTO_DELETE_EVERY_N_HOURS, WEBROOT } from "./helpers/env";
import { user } from "./pages/user";
import { root } from "./pages/root"
import { upload } from "./pages/upload"
import { history } from "./pages/history";
import { convert } from "./pages/convert"
import { download } from "./pages/download"
import { results } from "./pages/results";
import { deleteFile } from "./pages/deleteFile";
import { listConverters } from "./pages/listConverters";
import { chooseConverter } from "./pages/chooseConverter";
import db from "./db/db"; import db from "./db/db";
import { Jobs } from "./db/types"; import { Jobs } from "./db/types";
import { AUTO_DELETE_EVERY_N_HOURS, WEBROOT } from "./helpers/env";
import { chooseConverter } from "./pages/chooseConverter";
import { convert } from "./pages/convert";
import { deleteFile } from "./pages/deleteFile";
import { download } from "./pages/download";
import { history } from "./pages/history";
import { listConverters } from "./pages/listConverters";
import { results } from "./pages/results";
import { root } from "./pages/root";
import { upload } from "./pages/upload";
import { user } from "./pages/user";
mkdir("./data", { recursive: true }).catch(console.error); mkdir("./data", { recursive: true }).catch(console.error);
@ -64,19 +63,13 @@ if (process.env.NODE_ENV !== "production") {
app.listen(3000); app.listen(3000);
console.log( console.log(`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`);
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`,
);
const clearJobs = () => { const clearJobs = () => {
const jobs = db const jobs = db
.query("SELECT * FROM jobs WHERE date_created < ?") .query("SELECT * FROM jobs WHERE date_created < ?")
.as(Jobs) .as(Jobs)
.all( .all(new Date(Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000).toISOString());
new Date(
Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000,
).toISOString(),
);
for (const job of jobs) { for (const job of jobs) {
// delete the directories // delete the directories

View file

@ -1,75 +1,67 @@
import Elysia, { t } from "elysia";
import { userService } from "./user";
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { import Elysia, { t } from "elysia";
getPossibleTargets, import { getPossibleTargets } from "../converters/main";
} from "../converters/main"; import { userService } from "./user";
export const chooseConverter = new Elysia() export const chooseConverter = new Elysia().use(userService).post(
.use(userService) "/conversions",
.post( ({ body }) => {
"/conversions", return (
({ body }) => { <>
return ( <article
<> class={`
<article convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
class={` overflow-x-hidden overflow-y-auto rounded bg-neutral-800
convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col sm:h-[30vh]
overflow-x-hidden overflow-y-auto rounded bg-neutral-800 `}
sm:h-[30vh] >
`} {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
> <article
{Object.entries(getPossibleTargets(body.fileType)).map( class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4"
([converter, targets]) => ( data-converter={converter}
<article >
class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4" <header class="mb-2 w-full text-xl font-bold" safe>
data-converter={converter} {converter}
> </header>
<header class="mb-2 w-full text-xl font-bold" safe> <ul class="convert_to_target flex flex-row flex-wrap gap-1">
{converter} {targets.map((target) => (
</header> <button
<ul class="convert_to_target flex flex-row flex-wrap gap-1"> // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
{targets.map((target) => ( tabindex={0}
<button class={`
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953 target rounded bg-neutral-700 p-1 text-base
tabindex={0} hover:bg-neutral-600
class={` `}
target rounded bg-neutral-700 p-1 text-base data-value={`${target},${converter}`}
hover:bg-neutral-600 data-target={target}
`} data-converter={converter}
data-value={`${target},${converter}`} type="button"
data-target={target} safe
data-converter={converter} >
type="button" {target}
safe </button>
> ))}
{target} </ul>
</button> </article>
))} ))}
</ul> </article>
</article>
),
)}
</article>
<select name="convert_to" aria-label="Convert to" required hidden> <select name="convert_to" aria-label="Convert to" required hidden>
<option selected disabled value=""> <option selected disabled value="">
Convert to Convert to
</option> </option>
{Object.entries(getPossibleTargets(body.fileType)).map( {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
([converter, targets]) => ( <optgroup label={converter}>
<optgroup label={converter}> {targets.map((target) => (
{targets.map((target) => ( <option value={`${target},${converter}`} safe>
<option value={`${target},${converter}`} safe> {target}
{target} </option>
</option> ))}
))} </optgroup>
</optgroup> ))}
), </select>
)} </>
</select> );
</> },
); { body: t.Object({ fileType: t.String() }) },
}, );
{ body: t.Object({ fileType: t.String() }) },
)

View file

@ -3,14 +3,11 @@ import { Elysia, t } from "elysia";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import { outputDir, uploadsDir } from ".."; import { outputDir, uploadsDir } from "..";
import { mainConverter } from "../converters/main"; import { mainConverter } from "../converters/main";
import { WEBROOT } from "../helpers/env";
import db from "../db/db"; import db from "../db/db";
import {
normalizeFiletype,
normalizeOutputFiletype,
} from "../helpers/normalizeFiletype";
import { userService } from "./user";
import { Jobs } from "../db/types"; import { Jobs } from "../db/types";
import { WEBROOT } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
import { userService } from "./user";
export const convert = new Elysia().use(userService).post( export const convert = new Elysia().use(userService).post(
"/convert", "/convert",
@ -44,10 +41,7 @@ export const convert = new Elysia().use(userService).post(
try { try {
await mkdir(userOutputDir, { recursive: true }); await mkdir(userOutputDir, { recursive: true });
} catch (error) { } catch (error) {
console.error( console.error(`Failed to create the output directory: ${userOutputDir}.`, error);
`Failed to create the output directory: ${userOutputDir}.`,
error,
);
} }
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? ""); const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
@ -62,9 +56,10 @@ export const convert = new Elysia().use(userService).post(
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} }
db.query( db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run(
"UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2", fileNames.length,
).run(fileNames.length, jobId.value); jobId.value,
);
const query = db.query( const query = db.query(
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)", "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
@ -99,9 +94,7 @@ export const convert = new Elysia().use(userService).post(
.then(() => { .then(() => {
// All conversions are done, update the job status to 'completed' // All conversions are done, update the job status to 'completed'
if (jobId.value) { if (jobId.value) {
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run( db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
jobId.value,
);
} }
// delete all uploaded files in userUploadsDir // delete all uploaded files in userUploadsDir

View file

@ -1,39 +1,41 @@
import { Elysia, t } from "elysia";
import { userService } from "./user";
import { unlink } from "node:fs/promises"; import { unlink } from "node:fs/promises";
import { WEBROOT } from "../helpers/env"; import { Elysia, t } from "elysia";
import { uploadsDir } from ".."; import { uploadsDir } from "..";
import db from "../db/db"; import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const deleteFile = new Elysia() export const deleteFile = new Elysia().use(userService).post(
.use(userService) "/delete",
.post( async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
"/delete", if (!auth?.value) {
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { return redirect(`${WEBROOT}/login`, 302);
if (!auth?.value) { }
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
if (!jobId?.value) { if (!jobId?.value) {
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} }
const existingJob = await db const existingJob = await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?") .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.get(jobId.value, user.id); .get(jobId.value, user.id);
if (!existingJob) { if (!existingJob) {
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} }
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
await unlink(`${userUploadsDir}${body.filename}`); await unlink(`${userUploadsDir}${body.filename}`);
},
{ body: t.Object({ filename: t.String() }) }, return {
) message: "File deleted successfully.",
};
},
{ body: t.Object({ filename: t.String() }) },
);

View file

@ -1,10 +1,9 @@
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import { userService } from "./user";
import { WEBROOT } from "../helpers/env";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import { outputDir } from ".."; import { outputDir } from "..";
import db from "../db/db"; import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const download = new Elysia() export const download = new Elysia()
.use(userService) .use(userService)
@ -36,31 +35,28 @@ export const download = new Elysia()
return Bun.file(filePath); return Bun.file(filePath);
}, },
) )
.get( .get("/zip/:userId/:jobId", async ({ params, jwt, redirect, cookie: { auth } }) => {
"/zip/:userId/:jobId", // TODO: Implement zip download
async ({ params, jwt, redirect, cookie: { auth } }) => { if (!auth?.value) {
// TODO: Implement zip download return redirect(`${WEBROOT}/login`, 302);
if (!auth?.value) { }
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
const job = await db const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId); .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}/);
// return Bun.zip(outputPath); // return Bun.zip(outputPath);
}, });
)

View file

@ -1,11 +1,11 @@
import { Html } from "@elysiajs/html";
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Html } from "@elysiajs/html";
import { Header } from "../components/header"; import { Header } from "../components/header";
import { userService } from "./user";
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, WEBROOT } from "../helpers/env";
import { Filename, Jobs } from "../db/types";
import db from "../db/db"; import db from "../db/db";
import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const history = new Elysia() export const history = new Elysia()
.use(userService) .use(userService)
@ -23,17 +23,10 @@ export const history = new Elysia()
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
let userJobs = db let userJobs = db.query("SELECT * FROM jobs WHERE user_id = ?").as(Jobs).all(user.id).reverse();
.query("SELECT * FROM jobs WHERE user_id = ?")
.as(Jobs)
.all(user.id)
.reverse();
for (const job of userJobs) { for (const job of userJobs) {
const files = db const files = db.query("SELECT * FROM file_names WHERE job_id = ?").as(Filename).all(job.id);
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
.all(job.id);
job.finished_files = files.length; job.finished_files = files.length;
job.files_detailed = files; job.files_detailed = files;
@ -123,10 +116,7 @@ export const history = new Elysia()
{userJobs.map((job) => ( {userJobs.map((job) => (
<> <>
<tr id={`job-row-${job.id}`}> <tr id={`job-row-${job.id}`}>
<td <td class="job-details-toggle cursor-pointer" data-job-id={job.id}>
class="job-details-toggle cursor-pointer"
data-job-id={job.id}
>
<svg <svg
id={`arrow-${job.id}`} id={`arrow-${job.id}`}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -143,9 +133,7 @@ export const history = new Elysia()
/> />
</svg> </svg>
</td> </td>
<td safe> <td safe>{new Date(job.date_created).toLocaleTimeString()}</td>
{new Date(job.date_created).toLocaleTimeString()}
</td>
<td>{job.num_files}</td> <td>{job.num_files}</td>
<td class="max-sm:hidden">{job.finished_files}</td> <td class="max-sm:hidden">{job.finished_files}</td>
<td safe>{job.status}</td> <td safe>{job.status}</td>
@ -164,43 +152,29 @@ export const history = new Elysia()
<tr id={`details-${job.id}`} class="hidden"> <tr id={`details-${job.id}`} class="hidden">
<td colspan="6"> <td colspan="6">
<div class="p-2 text-sm text-neutral-500"> <div class="p-2 text-sm text-neutral-500">
<div class="mb-1 font-semibold"> <div class="mb-1 font-semibold">Detailed File Information:</div>
Detailed File Information: {job.files_detailed.map((file: Filename) => (
</div> <div class="flex items-center">
{job.files_detailed.map( <span class="w-5/12 truncate" title={file.file_name} safe>
(file: Filename) => ( {file.file_name}
<div </span>
class="flex items-center" <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="mx-2 inline-block h-4 w-4 text-neutral-500"
> >
<span <path
class="w-5/12 truncate" fill-rule="evenodd"
title={file.file_name} d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
safe clip-rule="evenodd"
> />
{file.file_name} </svg>
</span> <span class="w-5/12 truncate" title={file.output_file_name} safe>
<svg {file.output_file_name}
xmlns="http://www.w3.org/2000/svg" </span>
viewBox="0 0 20 20" </div>
fill="currentColor" ))}
class="mx-2 inline-block h-4 w-4 text-neutral-500"
>
<path
fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
<span
class="w-5/12 truncate"
title={file.output_file_name}
safe
>
{file.output_file_name}
</span>
</div>
),
)}
</div> </div>
</td> </td>
</tr> </tr>
@ -239,4 +213,4 @@ export const history = new Elysia()
</> </>
</BaseHtml> </BaseHtml>
); );
}) });

View file

@ -1,13 +1,10 @@
import Elysia from "elysia";
import { userService } from "./user";
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import Elysia from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Header } from "../components/header"; import { Header } from "../components/header";
import { import { getAllInputs, getAllTargets } from "../converters/main";
getAllInputs, import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
getAllTargets, import { userService } from "./user";
} from "../converters/main";
export const listConverters = new Elysia() export const listConverters = new Elysia()
.use(userService) .use(userService)
@ -24,11 +21,7 @@ export const listConverters = new Elysia()
return ( return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters"> <BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
<> <>
<Header <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
webroot={WEBROOT}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
loggedIn
/>
<main <main
class={` class={`
w-full flex-1 px-2 w-full flex-1 px-2
@ -53,32 +46,30 @@ export const listConverters = new Elysia()
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Object.entries(getAllTargets()).map( {Object.entries(getAllTargets()).map(([converter, targets]) => {
([converter, targets]) => { const inputs = getAllInputs(converter);
const inputs = getAllInputs(converter); return (
return ( <tr>
<tr> <td safe>{converter}</td>
<td safe>{converter}</td> <td>
<td> Count: {inputs.length}
Count: {inputs.length} <ul>
<ul> {inputs.map((input) => (
{inputs.map((input) => ( <li safe>{input}</li>
<li safe>{input}</li> ))}
))} </ul>
</ul> </td>
</td> <td>
<td> Count: {targets.length}
Count: {targets.length} <ul>
<ul> {targets.map((target) => (
{targets.map((target) => ( <li safe>{target}</li>
<li safe>{target}</li> ))}
))} </ul>
</ul> </td>
</td> </tr>
</tr> );
); })}
},
)}
</tbody> </tbody>
</table> </table>
</article> </article>
@ -86,4 +77,4 @@ export const listConverters = new Elysia()
</> </>
</BaseHtml> </BaseHtml>
); );
}) });

View file

@ -1,214 +1,215 @@
import { Elysia } from "elysia";
import { userService } from "./user";
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { Elysia } from "elysia";
import { Filename, Jobs } from "../db/types";
import { Header } from "../components/header";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db"; import db from "../db/db";
import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user";
function ResultsArticle({ job, files, outputPath }: { job: Jobs, files: Filename[], outputPath: string }) { function ResultsArticle({
return (<article class="article"> job,
<div class="mb-4 flex items-center justify-between"> files,
<h1 class="text-xl">Results</h1> outputPath,
<div> }: {
<button job: Jobs;
type="button" files: Filename[];
class="float-right w-40 btn-primary" outputPath: string;
onclick="downloadAll()" }) {
{...(files.length !== job.num_files return (
? { disabled: true, "aria-busy": "true" } <article class="article">
: "")} <div class="mb-4 flex items-center justify-between">
> <h1 class="text-xl">Results</h1>
{files.length === job.num_files <div>
? "Download All" <button
: "Converting..."} type="button"
</button> class="float-right w-40 btn-primary"
onclick="downloadAll()"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
>
{files.length === job.num_files ? "Download All" : "Converting..."}
</button>
</div>
</div> </div>
</div> <progress
<progress max={job.num_files}
max={job.num_files} value={files.length}
value={files.length} class={`
class={` mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500
bg-neutral-700 bg-none text-accent-500 accent-accent-500 [&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full
[&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none]
[&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-accent-500
[&[value]::-webkit-progress-value]:bg-accent-500 [&[value]::-webkit-progress-value]:transition-[inline-size]
[&[value]::-webkit-progress-value]:transition-[inline-size] `}
`} />
/> <table
<table class={` class={`
w-full table-auto rounded bg-neutral-900 text-left w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4 [&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800 [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`} `}
> >
<thead> <thead>
<tr>
<th class={`
px-2 py-2
sm:px-4
`}
>
Converted File Name
</th>
<th class={`
px-2 py-2
sm:px-4
`}
>
Status
</th>
<th class={`
px-2 py-2
sm:px-4
`}
>
View
</th>
<th class={`
px-2 py-2
sm:px-4
`}>
Download
</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr> <tr>
<td safe class="max-w-[20vw] truncate"> <th
{file.output_file_name} class={`
</td> px-2 py-2
<td safe>{file.status}</td> sm:px-4
<td> `}
<a >
class={` Converted File Name
text-accent-500 underline </th>
hover:text-accent-400 <th
`} class={`
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} px-2 py-2
> sm:px-4
View `}
</a> >
</td> Status
<td> </th>
<a <th
class={` class={`
text-accent-500 underline px-2 py-2
hover:text-accent-400 sm:px-4
`} `}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} >
download={file.output_file_name} View
> </th>
Download <th
</a> class={`
</td> px-2 py-2
sm:px-4
`}
>
Download
</th>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {files.map((file) => (
</article> <tr>
) <td safe class="max-w-[20vw] truncate">
{file.output_file_name}
</td>
<td safe>{file.status}</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
>
View
</a>
</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}
>
Download
</a>
</td>
</tr>
))}
</tbody>
</table>
</article>
);
} }
export const results = new Elysia() export const results = new Elysia()
.use(userService) .use(userService)
.get( .get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
"/results/:jobId", if (!auth?.value) {
async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => { return redirect(`${WEBROOT}/login`, 302);
if (!auth?.value) { }
return redirect(`${WEBROOT}/login`, 302);
}
if (job_id?.value) { if (job_id?.value) {
// clear the job_id cookie since we are viewing the results // clear the job_id cookie since we are viewing the results
job_id.remove(); job_id.remove();
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
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 <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
webroot={WEBROOT} <main
allowUnauthenticated={ALLOW_UNAUTHENTICATED} class={`
loggedIn w-full flex-1 px-2
/> sm:px-4
<main `}
class={` >
w-full flex-1 px-2 <ResultsArticle job={job} files={files} outputPath={outputPath} />
sm:px-4 </main>
`} <script src={`${WEBROOT}/results.js`} defer />
> </>
<ResultsArticle job={job} files={files} outputPath={outputPath} /> </BaseHtml>
</main> );
<script src={`${WEBROOT}/results.js`} defer /> })
</> .post("/progress/:jobId", async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
</BaseHtml> if (!auth?.value) {
); return redirect(`${WEBROOT}/login`, 302);
}, }
)
.post(
"/progress/:jobId",
async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
if (job_id?.value) { if (job_id?.value) {
// clear the job_id cookie since we are viewing the results // clear the job_id cookie since we are viewing the results
job_id.remove(); job_id.remove();
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
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 job={job} files={files} outputPath={outputPath} />; return <ResultsArticle job={job} files={files} outputPath={outputPath} />;
}, });
)

View file

@ -1,14 +1,20 @@
import { Elysia } from "elysia";
import { Html } from "@elysiajs/html";
import { FIRST_RUN, userService } from "./user";
import { ACCOUNT_REGISTRATION, ALLOW_UNAUTHENTICATED, HIDE_HISTORY, HTTP_ALLOWED, WEBROOT } from "../helpers/env";
import { JWTPayloadSpec } from "@elysiajs/jwt";
import { randomInt } from "node:crypto"; import { randomInt } from "node:crypto";
import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Header } from "../components/header"; import { Header } from "../components/header";
import { getAllTargets } from "../converters/main"; import { getAllTargets } from "../converters/main";
import db from "../db/db"; import db from "../db/db";
import { User } from "../db/types"; import { User } from "../db/types";
import {
ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED,
HIDE_HISTORY,
HTTP_ALLOWED,
WEBROOT,
} from "../helpers/env";
import { FIRST_RUN, userService } from "./user";
export const root = new Elysia() export const root = new Elysia()
.use(userService) .use(userService)
@ -27,10 +33,7 @@ export const root = new Elysia()
let user: ({ id: string } & JWTPayloadSpec) | false = false; let user: ({ id: string } & JWTPayloadSpec) | false = false;
if (ALLOW_UNAUTHENTICATED) { if (ALLOW_UNAUTHENTICATED) {
const newUserId = String( const newUserId = String(
randomInt( randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
2 ** 24,
Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER),
),
); );
const accessToken = await jwt.sign({ const accessToken = await jwt.sign({
id: newUserId, id: newUserId,
@ -54,20 +57,19 @@ export const root = new Elysia()
} else if (auth?.value) { } else if (auth?.value) {
user = await jwt.verify(auth.value); user = await jwt.verify(auth.value);
if (user !== false && user.id) { if (
if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) { user !== false &&
// make sure user exists in db user.id &&
const existingUser = db (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
.query("SELECT * FROM users WHERE id = ?") ) {
.as(User) // make sure user exists in db
.get(user.id); const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
if (!existingUser) { if (!existingUser) {
if (auth?.value) { if (auth?.value) {
auth.remove(); auth.remove();
}
return redirect(`${WEBROOT}/login`, 302);
} }
return redirect(`${WEBROOT}/login`, 302);
} }
} }
} }
@ -82,11 +84,9 @@ export const root = new Elysia()
new Date().toISOString(), new Date().toISOString(),
); );
const id = ( const { id } = db
db .query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
.query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC") .get(user.id) as { id: number };
.get(user.id) as { id: number }
).id;
if (!jobId) { if (!jobId) {
return { message: "Cookies should be enabled to use this app." }; return { message: "Cookies should be enabled to use this app." };
@ -172,62 +172,53 @@ export const root = new Elysia()
sm:h-[30vh] sm:h-[30vh]
`} `}
> >
{Object.entries(getAllTargets()).map( {Object.entries(getAllTargets()).map(([converter, targets]) => (
([converter, targets]) => ( <article
<article class={`
class={` convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
convert_to_group flex w-full flex-col border-b border-neutral-700 p-4 `}
`} data-converter={converter}
data-converter={converter} >
> <header class="mb-2 w-full text-xl font-bold" safe>
<header class="mb-2 w-full text-xl font-bold" safe> {converter}
{converter} </header>
</header> <ul class="convert_to_target flex flex-row flex-wrap gap-1">
<ul class="convert_to_target flex flex-row flex-wrap gap-1"> {targets.map((target) => (
{targets.map((target) => ( <button
<button // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953 tabindex={0}
tabindex={0} class={`
class={` target rounded bg-neutral-700 p-1 text-base
target rounded bg-neutral-700 p-1 text-base hover:bg-neutral-600
hover:bg-neutral-600 `}
`} data-value={`${target},${converter}`}
data-value={`${target},${converter}`} data-target={target}
data-target={target} data-converter={converter}
data-converter={converter} type="button"
type="button" safe
safe >
> {target}
{target} </button>
</button> ))}
))} </ul>
</ul> </article>
</article> ))}
),
)}
</article> </article>
{/* Hidden element which determines the format to convert the file too and the converter to use */} {/* Hidden element which determines the format to convert the file too and the converter to use */}
<select <select name="convert_to" aria-label="Convert to" required hidden>
name="convert_to"
aria-label="Convert to"
required
hidden
>
<option selected disabled value=""> <option selected disabled value="">
Convert to Convert to
</option> </option>
{Object.entries(getAllTargets()).map( {Object.entries(getAllTargets()).map(([converter, targets]) => (
([converter, targets]) => ( <optgroup label={converter}>
<optgroup label={converter}> {targets.map((target) => (
{targets.map((target) => ( <option value={`${target},${converter}`} safe>
<option value={`${target},${converter}`} safe> {target}
{target} </option>
</option> ))}
))} </optgroup>
</optgroup> ))}
),
)}
</select> </select>
</div> </div>
</article> </article>
@ -246,4 +237,4 @@ export const root = new Elysia()
</> </>
</BaseHtml> </BaseHtml>
); );
}) });

View file

@ -1,51 +1,48 @@
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { userService } from "./user"; import db from "../db/db";
import { WEBROOT } from "../helpers/env"; import { WEBROOT } from "../helpers/env";
import { uploadsDir } from "../index"; import { uploadsDir } from "../index";
import db from "../db/db"; import { userService } from "./user";
export const upload = new Elysia().use(userService).post(
"/upload",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
export const upload = new Elysia() const user = await jwt.verify(auth.value);
.use(userService) if (!user) {
.post( return redirect(`${WEBROOT}/login`, 302);
"/upload", }
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value); if (!jobId?.value) {
if (!user) { return redirect(`${WEBROOT}/`, 302);
return redirect(`${WEBROOT}/login`, 302); }
}
if (!jobId?.value) { const existingJob = await db
return redirect(`${WEBROOT}/`, 302); .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
} .get(jobId.value, user.id);
const existingJob = await db if (!existingJob) {
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?") return redirect(`${WEBROOT}/`, 302);
.get(jobId.value, user.id); }
if (!existingJob) { const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
return redirect(`${WEBROOT}/`, 302);
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; if (body?.file) {
if (Array.isArray(body.file)) {
if (body?.file) { for (const file of body.file) {
if (Array.isArray(body.file)) { await Bun.write(`${userUploadsDir}${file.name}`, file);
for (const file of body.file) {
await Bun.write(`${userUploadsDir}${file.name}`, file);
}
} else {
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
} }
} else {
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
} }
}
return { return {
message: "Files uploaded successfully.", message: "Files uploaded successfully.",
}; };
}, },
{ body: t.Object({ file: t.Files() }) }, { body: t.Object({ file: t.Files() }) },
) );

View file

@ -1,17 +1,22 @@
import { Elysia, t } from "elysia"; import { randomUUID } from "node:crypto";
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { jwt } from "@elysiajs/jwt";
import { Elysia, t } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Header } from "../components/header"; import { Header } from "../components/header";
import { jwt } from "@elysiajs/jwt";
import { randomUUID } from "node:crypto";
import { ACCOUNT_REGISTRATION, WEBROOT, HIDE_HISTORY, ALLOW_UNAUTHENTICATED, HTTP_ALLOWED } from "../helpers/env";
import db from "../db/db"; import db from "../db/db";
import { User } from "../db/types"; import { User } from "../db/types";
import {
ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED,
HIDE_HISTORY,
HTTP_ALLOWED,
WEBROOT,
} from "../helpers/env";
export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false; export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
export const userService = new Elysia({ name: 'user/service' }) export const userService = new Elysia({ name: "user/service" })
.use( .use(
jwt({ jwt({
name: "jwt", name: "jwt",
@ -25,35 +30,31 @@ export const userService = new Elysia({ name: 'user/service' })
.model({ .model({
signIn: t.Object({ signIn: t.Object({
email: t.String(), email: t.String(),
password: t.String() password: t.String(),
}), }),
}) })
.macro({ .macro({
isSignIn(enabled: boolean) { isSignIn(enabled: boolean) {
if (!enabled) return if (!enabled) return;
return { return {
async beforeHandle({ async beforeHandle({ status, jwt, cookie: { auth } }) {
status,
jwt,
cookie: { auth },
}) {
if (auth?.value) { if (auth?.value) {
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
return { return {
success: true, success: true,
user user,
} };
} }
return status(401, { return status(401, {
success: false, success: false,
message: 'Unauthorized' message: "Unauthorized",
}) });
} },
} };
} },
}) });
export const user = new Elysia() export const user = new Elysia()
.use(userService) .use(userService)
@ -72,9 +73,7 @@ export const user = new Elysia()
> >
<h1 class="my-8 text-3xl">Welcome to ConvertX!</h1> <h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
<article class="article p-0"> <article class="article p-0">
<header class="w-full bg-neutral-800 p-4"> <header class="w-full bg-neutral-800 p-4">Create your account</header>
Create your account
</header>
<form method="post" action={`${WEBROOT}/register`} class="p-4"> <form method="post" action={`${WEBROOT}/register`} class="p-4">
<fieldset class="mb-4 flex flex-col gap-4"> <fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1">
@ -166,11 +165,7 @@ export const user = new Elysia()
/> />
</label> </label>
</fieldset> </fieldset>
<input <input type="submit" value="Register" class="w-full btn-primary" />
type="submit"
value="Register"
class="w-full btn-primary"
/>
</form> </form>
</article> </article>
</main> </main>
@ -189,9 +184,7 @@ export const user = new Elysia()
FIRST_RUN = false; FIRST_RUN = false;
} }
const existingUser = await db const existingUser = await db.query("SELECT * FROM users WHERE email = ?").get(email);
.query("SELECT * FROM users WHERE email = ?")
.get(email);
if (existingUser) { if (existingUser) {
set.status = 400; set.status = 400;
return { return {
@ -200,15 +193,9 @@ export const user = new Elysia()
} }
const savedPassword = await Bun.password.hash(password); const savedPassword = await Bun.password.hash(password);
db.query("INSERT INTO users (email, password) VALUES (?, ?)").run( db.query("INSERT INTO users (email, password) VALUES (?, ?)").run(email, savedPassword);
email,
savedPassword,
);
const user = db const user = db.query("SELECT * FROM users WHERE email = ?").as(User).get(email);
.query("SELECT * FROM users WHERE email = ?")
.as(User)
.get(email);
if (!user) { if (!user) {
set.status = 500; set.status = 500;
@ -239,7 +226,7 @@ export const user = new Elysia()
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
}, },
{ body: 'signIn' }, { body: "signIn" },
) )
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => { .get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
if (FIRST_RUN) { if (FIRST_RUN) {
@ -308,11 +295,7 @@ export const user = new Elysia()
Register Register
</a> </a>
) : null} ) : null}
<input <input type="submit" value="Login" class="w-full btn-primary" />
type="submit"
value="Login"
class="w-full btn-primary"
/>
</div> </div>
</form> </form>
</article> </article>
@ -324,10 +307,7 @@ export const user = new Elysia()
.post( .post(
"/login", "/login",
async function handler({ body, set, redirect, jwt, cookie: { auth } }) { async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
const existingUser = db const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get(body.email);
.query("SELECT * FROM users WHERE email = ?")
.as(User)
.get(body.email);
if (!existingUser) { if (!existingUser) {
set.status = 403; set.status = 403;
@ -336,10 +316,7 @@ export const user = new Elysia()
}; };
} }
const validPassword = await Bun.password.verify( const validPassword = await Bun.password.verify(body.password, existingUser.password);
body.password,
existingUser.password,
);
if (!validPassword) { if (!validPassword) {
set.status = 403; set.status = 403;
@ -370,7 +347,7 @@ export const user = new Elysia()
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
}, },
{ body: 'signIn' }, { body: "signIn" },
) )
.get("/logoff", ({ redirect, cookie: { auth } }) => { .get("/logoff", ({ redirect, cookie: { auth } }) => {
if (auth?.value) { if (auth?.value) {
@ -396,10 +373,7 @@ export const user = new Elysia()
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} }
const userData = db const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
.query("SELECT * FROM users WHERE id = ?")
.as(User)
.get(user.id);
if (!userData) { if (!userData) {
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
@ -459,11 +433,7 @@ export const user = new Elysia()
</label> </label>
</fieldset> </fieldset>
<div role="group"> <div role="group">
<input <input type="submit" value="Update" class="w-full btn-primary" />
type="submit"
value="Update"
class="w-full btn-primary"
/>
</div> </div>
</form> </form>
</article> </article>
@ -483,10 +453,7 @@ export const user = new Elysia()
if (!user) { if (!user) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
const existingUser = db const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
.query("SELECT * FROM users WHERE id = ?")
.as(User)
.get(user.id);
if (!existingUser) { if (!existingUser) {
if (auth?.value) { if (auth?.value) {
@ -495,10 +462,7 @@ export const user = new Elysia()
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
const validPassword = await Bun.password.verify( const validPassword = await Bun.password.verify(body.password, existingUser.password);
body.password,
existingUser.password,
);
if (!validPassword) { if (!validPassword) {
set.status = 403; set.status = 403;
@ -542,4 +506,4 @@ export const user = new Elysia()
password: t.String(), password: t.String(),
}), }),
}, },
) );