chore: fix lint

This commit is contained in:
C4illin
2025-10-05 14:19:13 +00:00
parent d984891791
commit 17325a6e6d
87 changed files with 7624 additions and 7640 deletions

View File

@@ -1,21 +1,13 @@
# Development Dockerfile for ConvertX
FROM debian:trixie-slim FROM debian:trixie-slim
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
LABEL description="Development environment for ConvertX with all tools and dependencies"
WORKDIR /app WORKDIR /app
# Install system dependencies and development tools
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
# Basic tools
curl \ curl \
unzip \ unzip \
git \ git \
ca-certificates \ ca-certificates \
# Build tools
build-essential \ build-essential \
# ConvertX runtime dependencies
assimp-utils \ assimp-utils \
calibre \ calibre \
dasel \ dasel \
@@ -48,7 +40,6 @@ RUN apt-get update && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install Bun (JavaScript runtime and package manager)
RUN ARCH=$(uname -m) && \ RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \ if [ "$ARCH" = "aarch64" ]; then \
curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \ curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
@@ -59,7 +50,6 @@ RUN ARCH=$(uname -m) && \
rm bun-linux-*.zip && \ rm bun-linux-*.zip && \
chmod +x /usr/local/bin/bun chmod +x /usr/local/bin/bun
# Install VTracer binary for vector tracing
RUN ARCH=$(uname -m) && \ RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \ if [ "$ARCH" = "aarch64" ]; then \
VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \ VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \
@@ -72,15 +62,8 @@ RUN ARCH=$(uname -m) && \
chmod +x /usr/local/bin/vtracer && \ chmod +x /usr/local/bin/vtracer && \
rm /tmp/vtracer.tar.gz rm /tmp/vtracer.tar.gz
# Create data directory for development
RUN mkdir -p data RUN mkdir -p data
# Set environment variables for development
ENV NODE_ENV=development ENV NODE_ENV=development
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox" ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
# Expose the development port
EXPOSE 3000 EXPOSE 3000
# Default command for development
CMD ["bun", "run", "dev"] CMD ["bun", "run", "dev"]

View File

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

30
.github/FUNDING.yml vendored
View File

@@ -1,15 +1,15 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [C4illin] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: [C4illin] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,22 +1,22 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: "" title: ""
labels: bug labels: bug
assignees: "" assignees: ""
--- ---
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
**Checklist:** **Checklist:**
- [ ] I am accessing ConvertX over HTTPS or have `HTTP_ALLOWED=true` - [ ] I am accessing ConvertX over HTTPS or have `HTTP_ALLOWED=true`

View File

@@ -1,26 +1,26 @@
--- ---
name: Converter request name: Converter request
about: Suggest a converter for this project about: Suggest a converter for this project
title: "[Converter Request]" title: "[Converter Request]"
labels: "converter request" labels: "converter request"
assignees: "" assignees: ""
--- ---
**What file formats are missing?** **What file formats are missing?**
<!-- Provide an example of what you would like to convert --> <!-- Provide an example of what you would like to convert -->
**What converter should be added** **What converter should be added**
<!-- It has to be free and preferably open source --> <!-- It has to be free and preferably open source -->
**Are you willing to add it?** **Are you willing to add it?**
<!-- Adding a converter is very easy just copy one of the existing and modify it --> <!-- Adding a converter is very easy just copy one of the existing and modify it -->
- [ ] Yes - [ ] Yes
- [ ] No - [ ] No
**Additional context** **Additional context**
<!-- Add any other context or screenshots about the feature request here. --> <!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -1,13 +1,13 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: "[Feature Request]" title: "[Feature Request]"
labels: enhancement labels: enhancement
assignees: "" assignees: ""
--- ---
**Describe the solution you'd like** **Describe the solution you'd like**
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.
**Additional context** **Additional context**
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.

View File

@@ -1,31 +1,31 @@
name: Check Lint name: Check Lint
on: on:
push: push:
branches: ["main"] branches: ["main"]
pull_request: pull_request:
branches: ["main"] branches: ["main"]
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
lint: lint:
name: Run linting checks name: Run linting checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Set up Bun - name: Set up Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
with: with:
bun-version: 1.2.2 bun-version: 1.2.2
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
- name: Run lint - name: Run lint
run: bun run lint run: bun run lint

View File

@@ -1,173 +1,173 @@
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:
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
id-token: write id-token: write
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@v5 uses: actions/checkout@v5
- name: downcase REPO - name: downcase REPO
run: | run: |
echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}" echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
- 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: ghcr.io/${{ env.REPO }} images: ghcr.io/${{ env.REPO }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
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 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: Build and push by digest - name: Build and push by digest
id: build id: build
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
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=ghcr.io/${{ env.REPO }},push-by-digest=true,name-canonical=true,oci-mediatypes=true outputs: type=image,name=ghcr.io/${{ env.REPO }},push-by-digest=true,name-canonical=true,oci-mediatypes=true
push: ${{ github.event.pull_request.head.repo.full_name == github.repository }} push: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
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:
if: github.event.pull_request.head.repo.full_name == github.repository if: github.event.pull_request.head.repo.full_name == github.repository
name: Merge Docker manifests name: Merge Docker manifests
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
packages: write packages: write
attestations: write attestations: write
id-token: write id-token: write
needs: needs:
- build - build
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v5 uses: actions/download-artifact@v5
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digests-* pattern: digests-*
merge-multiple: true merge-multiple: true
- name: downcase REPO - name: downcase REPO
run: | run: |
echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}" echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
- 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: |
ghcr.io/${{ env.REPO }} ghcr.io/${{ env.REPO }}
${{ 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 'ghcr.io/${{ env.REPO }}@sha256:%s ' *) $(printf 'ghcr.io/${{ env.REPO }}@sha256:%s ' *)
- name: Inspect image - name: Inspect image
run: | run: |
docker buildx imagetools inspect 'ghcr.io/${{ env.REPO }}:${{ steps.meta.outputs.version }}' docker buildx imagetools inspect 'ghcr.io/${{ env.REPO }}:${{ steps.meta.outputs.version }}'

View File

@@ -1,27 +1,27 @@
name: Update Docker Hub Description name: Update Docker Hub Description
env: env:
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: c4illin DOCKERHUB_USERNAME: c4illin
on: on:
push: push:
branches: branches:
- main - main
paths: paths:
- README.md - README.md
- .github/workflows/dockerhub-description.yml - .github/workflows/dockerhub-description.yml
jobs: jobs:
dockerHubDescription: dockerHubDescription:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Docker Hub Description - name: Docker Hub Description
uses: peter-evans/dockerhub-description@v5 uses: peter-evans/dockerhub-description@v5
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

@@ -1,25 +1,25 @@
on: on:
push: push:
branches: branches:
- main - main
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
name: release-please name: release-please
jobs: jobs:
release-please: release-please:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: googleapis/release-please-action@v4 - uses: googleapis/release-please-action@v4
with: with:
# this assumes that you have created a personal access token # this assumes that you have created a personal access token
# (PAT) and configured it as a GitHub action secret named # (PAT) and configured it as a GitHub action secret named
# `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important). # `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }} token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
# token: ${{ secrets.GITHUB_TOKEN }} # token: ${{ secrets.GITHUB_TOKEN }}
# this is a built-in strategy in release-please, see "Action Inputs" # this is a built-in strategy in release-please, see "Action Inputs"
# for more options # for more options
release-type: node release-type: node

View File

@@ -1,21 +1,21 @@
name: Remove Docker Tag name: Remove Docker Tag
on: on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
remove-docker-tag: remove-docker-tag:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
# (required) # (required)
permissions: permissions:
contents: read contents: read
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,31 +1,31 @@
name: Check Tests name: Check Tests
on: on:
push: push:
branches: ["main"] branches: ["main"]
pull_request: pull_request:
branches: ["main"] branches: ["main"]
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
test: test:
name: Run tests name: Run tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Set up Bun - name: Set up Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
with: with:
bun-version: 1.2.2 bun-version: 1.2.2
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
- name: Run tests - name: Run tests
run: bun test run: bun test

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

@@ -1,233 +1,233 @@
# Changelog # Changelog
## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04) ## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04)
### Bug Fixes ### Bug Fixes
- change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311) - change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311)
## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03) ## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03)
### Features ### Features
- add dvisvgm ([625e1a5](https://github.com/C4illin/ConvertX/commit/625e1a51f620fe9da79d0127eb6c95f468d9ea2b)) - add dvisvgm ([625e1a5](https://github.com/C4illin/ConvertX/commit/625e1a51f620fe9da79d0127eb6c95f468d9ea2b))
- add ImageMagick ([b47e575](https://github.com/C4illin/ConvertX/commit/b47e5755f677056e8acecad54c0c2e28a5e137f3)), closes [#295](https://github.com/C4illin/ConvertX/issues/295), closes [#269](https://github.com/C4illin/ConvertX/issues/269) - add ImageMagick ([b47e575](https://github.com/C4illin/ConvertX/commit/b47e5755f677056e8acecad54c0c2e28a5e137f3)), closes [#295](https://github.com/C4illin/ConvertX/issues/295), closes [#269](https://github.com/C4illin/ConvertX/issues/269)
- enhance job details display with file information ([50725ed](https://github.com/C4illin/ConvertX/commit/50725edd021bb9a7f58c85b79c1eab355ad22ced)), closes [#251](https://github.com/C4illin/ConvertX/issues/251) - enhance job details display with file information ([50725ed](https://github.com/C4illin/ConvertX/commit/50725edd021bb9a7f58c85b79c1eab355ad22ced)), closes [#251](https://github.com/C4illin/ConvertX/issues/251)
- improve job details interaction and accessibility ([29ba229](https://github.com/C4illin/ConvertX/commit/29ba229bc23d2019d2ee9829da7852f884ffa611)) - improve job details interaction and accessibility ([29ba229](https://github.com/C4illin/ConvertX/commit/29ba229bc23d2019d2ee9829da7852f884ffa611))
- show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26)) - show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26))
### Bug Fixes ### Bug Fixes
- add av1 and h26X with containers ([af5c768](https://github.com/C4illin/ConvertX/commit/af5c768dc74b3124fd7ef4b29e27c83a5d19ad49)), closes [#287](https://github.com/C4illin/ConvertX/issues/287), closes [#293](https://github.com/C4illin/ConvertX/issues/293) - add av1 and h26X with containers ([af5c768](https://github.com/C4illin/ConvertX/commit/af5c768dc74b3124fd7ef4b29e27c83a5d19ad49)), closes [#287](https://github.com/C4illin/ConvertX/issues/287), closes [#293](https://github.com/C4illin/ConvertX/issues/293)
- progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7)) - progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7))
- register button style ([b9bbf77](https://github.com/C4illin/ConvertX/commit/b9bbf7792f01fcaa77e3520925de107e856926f1)) - register button style ([b9bbf77](https://github.com/C4illin/ConvertX/commit/b9bbf7792f01fcaa77e3520925de107e856926f1))
- switch from alpine to debian trixie ([4e4c029](https://github.com/C4illin/ConvertX/commit/4e4c029cb800df86affb99c3a82dda9e6708bdde)), closes [#234](https://github.com/C4illin/ConvertX/issues/234), closes [#199](https://github.com/C4illin/ConvertX/issues/199) - switch from alpine to debian trixie ([4e4c029](https://github.com/C4illin/ConvertX/commit/4e4c029cb800df86affb99c3a82dda9e6708bdde)), closes [#234](https://github.com/C4illin/ConvertX/issues/234), closes [#199](https://github.com/C4illin/ConvertX/issues/199)
## [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))

310
README.md
View File

@@ -1,155 +1,155 @@
![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 - Convert files to different formats
- Process multiple files at once - Process multiple files at once
- Password protection - Password protection
- Multiple accounts - 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 | | [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 | | [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 | | [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 | | [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 | | [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 | | [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 | | [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [dvisvgm](https://dvisvgm.de/) | Vector images | 4 | 2 | | [dvisvgm](https://dvisvgm.de/) | Vector images | 4 | 2 |
| [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 | | [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 | | [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 |
| [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 | | [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 | | [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 | | [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 |
| [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 | | [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 |
| [VTracer](https://github.com/visioncortex/vtracer) | Raster to vector | 8 | 1 | | [VTracer](https://github.com/visioncortex/vtracer) | Raster to vector | 8 | 1 |
| [Dasel](https://github.com/TomWright/dasel) | Data Files | 5 | 4 | | [Dasel](https://github.com/TomWright/dasel) | Data Files | 5 | 4 |
<!-- 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
# - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection # - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection
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 |
| LANGUAGE | en | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) | | LANGUAGE | en | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) |
| UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users | | UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users |
### 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] > [!NOTE]
> These are written by other people, and may be outdated, incorrect or wrong. > 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 0. Install [Bun](https://bun.sh/) and Git
1. Clone the repository 1. Clone the repository
2. `bun install` 2. `bun install`
3. `bun run dev` 3. `bun run dev`
Pull requests are welcome! See open issues for the list of todos. The ones tagged with "converter request" are quite easy. Help with docs and cleaning up in issues are also very welcome! Pull requests are welcome! See open issues for the list of todos. The ones tagged with "converter request" are quite easy. Help with docs and cleaning up in issues are also very welcome!
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.
## Contributors ## Contributors
<a href="https://github.com/C4illin/ConvertX/graphs/contributors"> <a href="https://github.com/C4illin/ConvertX/graphs/contributors">
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/> <img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
</a> </a>
## Star History ## Star History
<a href="https://github.com/C4illin/ConvertX/stargazers"> <a href="https://github.com/C4illin/ConvertX/stargazers">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date&theme=dark" /> <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="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" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
</picture> </picture>
</a> </a>

View File

@@ -1,9 +1,9 @@
# Security Policy # Security Policy
## Supported Versions ## Supported Versions
Only the latest release is supported Only the latest release is supported
## Reporting a Vulnerability ## Reporting a Vulnerability
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab. To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab.

View File

@@ -1,72 +1,72 @@
{ {
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"formatWithErrors": true, "formatWithErrors": true,
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 2, "indentWidth": 2,
"lineEnding": "lf", "lineEnding": "lf",
"lineWidth": 80, "lineWidth": 80,
"attributePosition": "auto" "attributePosition": "auto"
}, },
"files": { "files": {
"ignore": ["**/node_modules/**", "**/pico.lime.min.css"] "ignore": ["**/node_modules/**", "**/pico.lime.min.css"]
}, },
"organizeImports": { "organizeImports": {
"enabled": true "enabled": true
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true, "recommended": true,
"complexity": { "complexity": {
"noBannedTypes": "error", "noBannedTypes": "error",
"noUselessThisAlias": "error", "noUselessThisAlias": "error",
"noUselessTypeConstraint": "error", "noUselessTypeConstraint": "error",
"useArrowFunction": "off", "useArrowFunction": "off",
"useLiteralKeys": "error", "useLiteralKeys": "error",
"useOptionalChain": "error" "useOptionalChain": "error"
}, },
"correctness": { "correctness": {
"noPrecisionLoss": "error", "noPrecisionLoss": "error",
"noUnusedVariables": "off", "noUnusedVariables": "off",
"useJsxKeyInIterable": "off" "useJsxKeyInIterable": "off"
}, },
"style": { "style": {
"noInferrableTypes": "error", "noInferrableTypes": "error",
"noNamespace": "error", "noNamespace": "error",
"useAsConstAssertion": "error", "useAsConstAssertion": "error",
"useBlockStatements": "off", "useBlockStatements": "off",
"useConsistentArrayType": "error", "useConsistentArrayType": "error",
"useForOf": "error", "useForOf": "error",
"useImportType": "error", "useImportType": "error",
"useShorthandFunctionType": "error" "useShorthandFunctionType": "error"
}, },
"suspicious": { "suspicious": {
"noEmptyBlockStatements": "error", "noEmptyBlockStatements": "error",
"noEmptyInterface": "error", "noEmptyInterface": "error",
"noExplicitAny": "warn", "noExplicitAny": "warn",
"noExtraNonNullAssertion": "error", "noExtraNonNullAssertion": "error",
"noMisleadingInstantiator": "error", "noMisleadingInstantiator": "error",
"noUnsafeDeclarationMerging": "error", "noUnsafeDeclarationMerging": "error",
"useAwait": "error", "useAwait": "error",
"useNamespaceKeyword": "error" "useNamespaceKeyword": "error"
}, },
"nursery": { "nursery": {
"useSortedClasses": "error" "useSortedClasses": "error"
} }
} }
}, },
"javascript": { "javascript": {
"formatter": { "formatter": {
"jsxQuoteStyle": "double", "jsxQuoteStyle": "double",
"quoteProperties": "asNeeded", "quoteProperties": "asNeeded",
"semicolons": "always", "semicolons": "always",
"arrowParentheses": "always", "arrowParentheses": "always",
"bracketSpacing": true, "bracketSpacing": true,
"bracketSameLine": true, "bracketSameLine": true,
"quoteStyle": "double", "quoteStyle": "double",
"attributePosition": "auto" "attributePosition": "auto"
} }
} }
} }

View File

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

View File

@@ -1,20 +1,20 @@
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
# - UNAUTHENTICATED_USER_SHARING=true # for use with ALLOW_UNAUTHENTICATED=true to share history with all unauthenticated users / devices # - UNAUTHENTICATED_USER_SHARING=true # for use with ALLOW_UNAUTHENTICATED=true to share history with all unauthenticated users / devices
ports: ports:
- 3000:3000 - 3000:3000

View File

@@ -1,72 +1,72 @@
import js from "@eslint/js"; import js from "@eslint/js";
import eslintParserTypeScript from "@typescript-eslint/parser"; import eslintParserTypeScript from "@typescript-eslint/parser";
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 tseslint.config( export default tseslint.config(
js.configs.recommended, js.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommended,
{ {
plugins: { plugins: {
"simple-import-sort": simpleImportSortPlugin, "simple-import-sort": simpleImportSortPlugin,
"better-tailwindcss": eslintPluginBetterTailwindcss, "better-tailwindcss": eslintPluginBetterTailwindcss,
}, },
ignores: ["**/node_modules/**", "eslint.config.ts"], ignores: ["**/node_modules/**", "eslint.config.ts"],
languageOptions: { languageOptions: {
parser: eslintParserTypeScript, parser: eslintParserTypeScript,
parserOptions: { parserOptions: {
project: true, project: true,
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
}, },
}, },
globals: { globals: {
...globals.node, ...globals.node,
}, },
}, },
files: ["**/*.{tsx,ts}"], files: ["**/*.{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/enforce-consistent-line-wrapping": [ "better-tailwindcss/enforce-consistent-line-wrapping": [
"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",
], ],
}, },
], ],
}, },
}, },
{ {
files: ["**/*.{js,cjs,mjs,jsx}"], files: ["**/*.{js,cjs,mjs,jsx}"],
extends: [tseslint.configs.disableTypeChecked], extends: [tseslint.configs.disableTypeChecked],
languageOptions: { languageOptions: {
globals: { globals: {
...globals.browser, ...globals.browser,
}, },
}, },
}, },
); );

View File

@@ -1,9 +1,9 @@
{ {
"$schema": "https://unpkg.com/knip@5/schema.json", "$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["tests/**/*.test.ts"], "entry": ["tests/**/*.test.ts"],
"project": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"], "project": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"],
"tailwind": { "tailwind": {
"entry": ["src/main.css"] "entry": ["src/main.css"]
}, },
"ignoreDependencies": ["tailwind-scrollbar"] "ignoreDependencies": ["tailwind-scrollbar"]
} }

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
/** /**
* @type {import('prettier').Config & import("@ianvs/prettier-plugin-sort-imports").PluginConfig} * @type {import('prettier').Config & import("@ianvs/prettier-plugin-sort-imports").PluginConfig}
*/ */
const config = { const config = {
arrowParens: "always", arrowParens: "always",
printWidth: 100, printWidth: 100,
singleQuote: false, singleQuote: false,
semi: true, semi: true,
tabWidth: 2, tabWidth: 2,
plugins: ["@ianvs/prettier-plugin-sort-imports"], plugins: ["@ianvs/prettier-plugin-sort-imports"],
}; };
export default config; export default config;

View File

@@ -1,24 +1,24 @@
const webroot = document.querySelector("meta[name='webroot']").content; const webroot = document.querySelector("meta[name='webroot']").content;
const jobId = window.location.pathname.split("/").pop(); const jobId = window.location.pathname.split("/").pop();
const main = document.querySelector("main"); const main = document.querySelector("main");
let progressElem = document.querySelector("progress"); let progressElem = document.querySelector("progress");
const refreshData = () => { const refreshData = () => {
// console.log("Refreshing data...", progressElem.value, progressElem.max); // console.log("Refreshing data...", progressElem.value, progressElem.max);
if (progressElem.value !== progressElem.max) { if (progressElem.value !== progressElem.max) {
fetch(`${webroot}/progress/${jobId}`, { fetch(`${webroot}/progress/${jobId}`, {
method: "POST", method: "POST",
}) })
.then((res) => res.text()) .then((res) => res.text())
.then((html) => { .then((html) => {
main.innerHTML = html; main.innerHTML = html;
}) })
.catch((err) => console.log(err)); .catch((err) => console.log(err));
setTimeout(refreshData, 1000); setTimeout(refreshData, 1000);
} }
progressElem = document.querySelector("progress"); progressElem = document.querySelector("progress");
}; };
refreshData(); refreshData();

View File

@@ -1,251 +1,251 @@
const webroot = document.querySelector("meta[name='webroot']").content; const webroot = document.querySelector("meta[name='webroot']").content;
const fileInput = document.querySelector('input[type="file"]'); const fileInput = document.querySelector('input[type="file"]');
const dropZone = document.getElementById("dropzone"); const dropZone = document.getElementById("dropzone");
const convertButton = document.querySelector("input[type='submit']"); const convertButton = document.querySelector("input[type='submit']");
const fileNames = []; const fileNames = [];
let fileType; let fileType;
let pendingFiles = 0; let pendingFiles = 0;
let formatSelected = false; let formatSelected = false;
dropZone.addEventListener("dragover", (e) => { dropZone.addEventListener("dragover", (e) => {
e.preventDefault(); e.preventDefault();
dropZone.classList.add("dragover"); dropZone.classList.add("dragover");
}); });
dropZone.addEventListener("dragleave", () => { dropZone.addEventListener("dragleave", () => {
dropZone.classList.remove("dragover"); dropZone.classList.remove("dragover");
}); });
dropZone.addEventListener("drop", (e) => { dropZone.addEventListener("drop", (e) => {
e.preventDefault(); e.preventDefault();
dropZone.classList.remove("dragover"); dropZone.classList.remove("dragover");
const files = e.dataTransfer.files; const files = e.dataTransfer.files;
if (files.length === 0) { if (files.length === 0) {
console.warn("No files dropped — likely a URL or unsupported source."); console.warn("No files dropped — likely a URL or unsupported source.");
return; return;
} }
for (const file of files) { for (const file of files) {
console.log("Handling dropped file:", file.name); console.log("Handling dropped file:", file.name);
handleFile(file); handleFile(file);
} }
}); });
// Extracted handleFile function for reusability in drag-and-drop and file input // Extracted handleFile function for reusability in drag-and-drop and file input
function handleFile(file) { function handleFile(file) {
const fileList = document.querySelector("#file-list"); const fileList = document.querySelector("#file-list");
const row = document.createElement("tr"); const row = document.createElement("tr");
row.innerHTML = ` row.innerHTML = `
<td>${file.name}</td> <td>${file.name}</td>
<td><progress max="100" class="inline-block h-2 appearance-none overflow-hidden rounded-full border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500 [&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-accent-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"></progress></td> <td><progress max="100" class="inline-block h-2 appearance-none overflow-hidden rounded-full border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500 [&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-accent-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"></progress></td>
<td>${(file.size / 1024).toFixed(2)} kB</td> <td>${(file.size / 1024).toFixed(2)} kB</td>
<td><a onclick="deleteRow(this)">Remove</a></td> <td><a onclick="deleteRow(this)">Remove</a></td>
`; `;
if (!fileType) { if (!fileType) {
fileType = file.name.split(".").pop(); fileType = file.name.split(".").pop();
fileInput.setAttribute("accept", `.${fileType}`); fileInput.setAttribute("accept", `.${fileType}`);
setTitle(); setTitle();
fetch(`${webroot}/conversions`, { fetch(`${webroot}/conversions`, {
method: "POST", method: "POST",
body: JSON.stringify({ fileType }), body: JSON.stringify({ fileType }),
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}) })
.then((res) => res.text()) .then((res) => res.text())
.then((html) => { .then((html) => {
selectContainer.innerHTML = html; selectContainer.innerHTML = html;
updateSearchBar(); updateSearchBar();
}) })
.catch(console.error); .catch(console.error);
} }
fileList.appendChild(row); fileList.appendChild(row);
file.htmlRow = row; file.htmlRow = row;
fileNames.push(file.name); fileNames.push(file.name);
uploadFile(file); uploadFile(file);
} }
const selectContainer = document.querySelector("form .select_container"); const selectContainer = document.querySelector("form .select_container");
const updateSearchBar = () => { const updateSearchBar = () => {
const convertToInput = document.querySelector("input[name='convert_to_search']"); const convertToInput = document.querySelector("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 = {};
const convertToElement = document.querySelector("select[name='convert_to']"); const convertToElement = document.querySelector("select[name='convert_to']");
const showMatching = (search) => { const showMatching = (search) => {
for (const [targets, groupElement] of Object.values(convertToGroups)) { for (const [targets, groupElement] of Object.values(convertToGroups)) {
let matchingTargetsFound = 0; let matchingTargetsFound = 0;
for (const target of targets) { for (const target of targets) {
if (target.dataset.target.includes(search)) { if (target.dataset.target.includes(search)) {
matchingTargetsFound++; matchingTargetsFound++;
target.classList.remove("hidden"); target.classList.remove("hidden");
target.classList.add("flex"); target.classList.add("flex");
} else { } else {
target.classList.add("hidden"); target.classList.add("hidden");
target.classList.remove("flex"); target.classList.remove("flex");
} }
} }
if (matchingTargetsFound === 0) { if (matchingTargetsFound === 0) {
groupElement.classList.add("hidden"); groupElement.classList.add("hidden");
groupElement.classList.remove("flex"); groupElement.classList.remove("flex");
} else { } else {
groupElement.classList.remove("hidden"); groupElement.classList.remove("hidden");
groupElement.classList.add("flex"); groupElement.classList.add("flex");
} }
} }
}; };
for (const groupElement of convertToGroupElements) { for (const groupElement of convertToGroupElements) {
const groupName = groupElement.dataset.converter; const groupName = groupElement.dataset.converter;
const targetElements = groupElement.querySelectorAll(".target"); const targetElements = groupElement.querySelectorAll(".target");
const targets = Array.from(targetElements); const targets = Array.from(targetElements);
for (const target of targets) { for (const target of targets) {
target.onmousedown = () => { target.onmousedown = () => {
convertToElement.value = target.dataset.value; convertToElement.value = target.dataset.value;
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`; convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
formatSelected = true; formatSelected = true;
if (pendingFiles === 0 && fileNames.length > 0) { if (pendingFiles === 0 && fileNames.length > 0) {
convertButton.disabled = false; convertButton.disabled = false;
} }
showMatching(""); showMatching("");
}; };
} }
convertToGroups[groupName] = [targets, groupElement]; convertToGroups[groupName] = [targets, groupElement];
} }
convertToInput.addEventListener("input", (e) => { convertToInput.addEventListener("input", (e) => {
showMatching(e.target.value.toLowerCase()); showMatching(e.target.value.toLowerCase());
}); });
convertToInput.addEventListener("search", () => { convertToInput.addEventListener("search", () => {
// when the user clears the search bar using the 'x' button // when the user clears the search bar using the 'x' button
convertButton.disabled = true; convertButton.disabled = true;
formatSelected = false; formatSelected = false;
}); });
convertToInput.addEventListener("blur", (e) => { convertToInput.addEventListener("blur", (e) => {
// Keep the popup open even when clicking on a target button // Keep the popup open even when clicking on a target button
// for a split second to allow the click to go through // for a split second to allow the click to go through
if (e?.relatedTarget?.classList?.contains("target")) { if (e?.relatedTarget?.classList?.contains("target")) {
convertToPopup.classList.add("hidden"); convertToPopup.classList.add("hidden");
convertToPopup.classList.remove("flex"); convertToPopup.classList.remove("flex");
return; return;
} }
convertToPopup.classList.add("hidden"); convertToPopup.classList.add("hidden");
convertToPopup.classList.remove("flex"); convertToPopup.classList.remove("flex");
}); });
convertToInput.addEventListener("focus", () => { convertToInput.addEventListener("focus", () => {
convertToPopup.classList.remove("hidden"); convertToPopup.classList.remove("hidden");
convertToPopup.classList.add("flex"); convertToPopup.classList.add("flex");
}); });
}; };
// Add a 'change' event listener to the file input element // Add a 'change' event listener to the file input element
fileInput.addEventListener("change", (e) => { fileInput.addEventListener("change", (e) => {
const files = e.target.files; const files = e.target.files;
for (const file of files) { for (const file of files) {
handleFile(file); handleFile(file);
} }
}); });
const setTitle = () => { const setTitle = () => {
const title = document.querySelector("h1"); const title = document.querySelector("h1");
title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`; title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
}; };
// Add a onclick for the delete button // Add a onclick for the delete button
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const deleteRow = (target) => { const deleteRow = (target) => {
const filename = target.parentElement.parentElement.children[0].textContent; const filename = target.parentElement.parentElement.children[0].textContent;
const row = target.parentElement.parentElement; const row = target.parentElement.parentElement;
row.remove(); row.remove();
// remove from fileNames // remove from fileNames
const index = fileNames.indexOf(filename); const index = fileNames.indexOf(filename);
fileNames.splice(index, 1); fileNames.splice(index, 1);
// reset fileInput // reset fileInput
fileInput.value = ""; fileInput.value = "";
// if fileNames is empty, reset fileType // if fileNames is empty, reset fileType
if (fileNames.length === 0) { if (fileNames.length === 0) {
fileType = null; fileType = null;
fileInput.removeAttribute("accept"); fileInput.removeAttribute("accept");
convertButton.disabled = true; convertButton.disabled = true;
setTitle(); setTitle();
} }
fetch(`${webroot}/delete`, { fetch(`${webroot}/delete`, {
method: "POST", method: "POST",
body: JSON.stringify({ filename: filename }), body: JSON.stringify({ filename: filename }),
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) => {
convertButton.disabled = true; convertButton.disabled = true;
convertButton.textContent = "Uploading..."; convertButton.textContent = "Uploading...";
pendingFiles += 1; pendingFiles += 1;
const formData = new FormData(); const formData = new FormData();
formData.append("file", file, file.name); formData.append("file", file, file.name);
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.open("POST", `${webroot}/upload`, true); xhr.open("POST", `${webroot}/upload`, true);
xhr.onload = () => { xhr.onload = () => {
let data = JSON.parse(xhr.responseText); let data = JSON.parse(xhr.responseText);
pendingFiles -= 1; pendingFiles -= 1;
if (pendingFiles === 0) { if (pendingFiles === 0) {
if (formatSelected) { if (formatSelected) {
convertButton.disabled = false; convertButton.disabled = false;
} }
convertButton.textContent = "Convert"; convertButton.textContent = "Convert";
} }
//Remove the progress bar when upload is done //Remove the progress bar when upload is done
let progressbar = file.htmlRow.getElementsByTagName("progress"); let progressbar = file.htmlRow.getElementsByTagName("progress");
progressbar[0].parentElement.remove(); progressbar[0].parentElement.remove();
console.log(data); console.log(data);
}; };
xhr.upload.onprogress = (e) => { xhr.upload.onprogress = (e) => {
let sent = e.loaded; let sent = e.loaded;
let total = e.total; let total = e.total;
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) => {
console.log(e); console.log(e);
}; };
xhr.send(formData); xhr.send(formData);
}; };
const formConvert = document.querySelector(`form[action='${webroot}/convert']`); const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
formConvert.addEventListener("submit", () => { formConvert.addEventListener("submit", () => {
const hiddenInput = document.querySelector("input[name='file_names']"); const hiddenInput = document.querySelector("input[name='file_names']");
hiddenInput.value = JSON.stringify(fileNames); hiddenInput.value = JSON.stringify(fileNames);
}); });
updateSearchBar(); updateSearchBar();

View File

@@ -1,19 +1,19 @@
{ {
"name": "ConvertX | Self Hosted File Converter", "name": "ConvertX | Self Hosted File Converter",
"short_name": "ConvertX", "short_name": "ConvertX",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/android-chrome-512x512.png", "src": "/android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#a5d601", "theme_color": "#a5d601",
"background_color": "#13171f", "background_color": "#13171f",
"display": "standalone" "display": "standalone"
} }

View File

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

2
reset.d.ts vendored
View File

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

View File

@@ -1,44 +1,44 @@
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { version } from "../../package.json"; import { version } from "../../package.json";
export const BaseHtml = ({ export const BaseHtml = ({
children, children,
title = "ConvertX", title = "ConvertX",
webroot = "", webroot = "",
}: { }: {
children: JSX.Element; children: JSX.Element;
title?: string; title?: string;
webroot?: string; webroot?: string;
}) => ( }) => (
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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 rel="apple-touch-icon" sizes="180x180" href={`${webroot}/apple-touch-icon.png`} /> <link rel="apple-touch-icon" sizes="180x180" 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="32x32" href={`${webroot}/favicon-32x32.png`} />
<link rel="icon" type="image/png" sizes="16x16" href={`${webroot}/favicon-16x16.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`}>
{children} {children}
<footer class="w-full"> <footer class="w-full">
<div class="p-4 text-center text-sm text-neutral-500"> <div class="p-4 text-center text-sm text-neutral-500">
<span>Powered by </span> <span>Powered by </span>
<a <a
href="https://github.com/C4illin/ConvertX" href="https://github.com/C4illin/ConvertX"
class={` class={`
text-neutral-400 text-neutral-400
hover:text-accent-500 hover:text-accent-500
`} `}
> >
ConvertX{" "} ConvertX{" "}
</a> </a>
<span safe>v{version || ""}</span> <span safe>v{version || ""}</span>
</div> </div>
</footer> </footer>
</body> </body>
</html> </html>
); );

View File

@@ -1,106 +1,106 @@
import { Html } from "@kitajs/html"; import { Html } from "@kitajs/html";
export const Header = ({ export const Header = ({
loggedIn, loggedIn,
accountRegistration, accountRegistration,
allowUnauthenticated, allowUnauthenticated,
hideHistory, hideHistory,
webroot = "", webroot = "",
}: { }: {
loggedIn?: boolean; loggedIn?: boolean;
accountRegistration?: boolean; accountRegistration?: boolean;
allowUnauthenticated?: boolean; allowUnauthenticated?: boolean;
hideHistory?: boolean; hideHistory?: boolean;
webroot?: string; webroot?: string;
}) => { }) => {
let rightNav: JSX.Element; let rightNav: JSX.Element;
if (loggedIn) { if (loggedIn) {
rightNav = ( rightNav = (
<ul class="flex gap-4"> <ul class="flex gap-4">
{!hideHistory && ( {!hideHistory && (
<li> <li>
<a <a
class={` class={`
text-accent-600 transition-all text-accent-600 transition-all
hover:text-accent-500 hover:underline hover:text-accent-500 hover:underline
`} `}
href={`${webroot}/history`} href={`${webroot}/history`}
> >
History History
</a> </a>
</li> </li>
)} )}
{!allowUnauthenticated ? ( {!allowUnauthenticated ? (
<li> <li>
<a <a
class={` class={`
text-accent-600 transition-all text-accent-600 transition-all
hover:text-accent-500 hover:underline hover:text-accent-500 hover:underline
`} `}
href={`${webroot}/account`} href={`${webroot}/account`}
> >
Account Account
</a> </a>
</li> </li>
) : null} ) : null}
{!allowUnauthenticated ? ( {!allowUnauthenticated ? (
<li> <li>
<a <a
class={` class={`
text-accent-600 transition-all text-accent-600 transition-all
hover:text-accent-500 hover:underline hover:text-accent-500 hover:underline
`} `}
href={`${webroot}/logoff`} href={`${webroot}/logoff`}
> >
Logout Logout
</a> </a>
</li> </li>
) : null} ) : null}
</ul> </ul>
); );
} else { } else {
rightNav = ( rightNav = (
<ul class="flex gap-4"> <ul class="flex gap-4">
<li> <li>
<a <a
class={` class={`
text-accent-600 transition-all text-accent-600 transition-all
hover:text-accent-500 hover:underline hover:text-accent-500 hover:underline
`} `}
href={`${webroot}/login`} href={`${webroot}/login`}
> >
Login Login
</a> </a>
</li> </li>
{accountRegistration ? ( {accountRegistration ? (
<li> <li>
<a <a
class={` class={`
text-accent-600 transition-all text-accent-600 transition-all
hover:text-accent-500 hover:underline hover:text-accent-500 hover:underline
`} `}
href={`${webroot}/register`} href={`${webroot}/register`}
> >
Register Register
</a> </a>
</li> </li>
) : null} ) : null}
</ul> </ul>
); );
} }
return ( return (
<header class="w-full p-4"> <header class="w-full p-4">
<nav class={`mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4`}> <nav class={`mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4`}>
<ul> <ul>
<li> <li>
<strong> <strong>
<a href={`${webroot}/`}>ConvertX</a> <a href={`${webroot}/`}>ConvertX</a>
</strong> </strong>
</li> </li>
</ul> </ul>
{rightNav} {rightNav}
</nav> </nav>
</header> </header>
); );
}; };

View File

@@ -1,140 +1,140 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
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,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): 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

@@ -1,86 +1,86 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
document: [ document: [
"azw4", "azw4",
"chm", "chm",
"cbr", "cbr",
"cbz", "cbz",
"cbt", "cbt",
"cba", "cba",
"cb7", "cb7",
"djvu", "djvu",
"docx", "docx",
"epub", "epub",
"fb2", "fb2",
"htlz", "htlz",
"html", "html",
"lit", "lit",
"lrf", "lrf",
"mobi", "mobi",
"odt", "odt",
"pdb", "pdb",
"pdf", "pdf",
"pml", "pml",
"rb", "rb",
"rtf", "rtf",
"recipe", "recipe",
"snb", "snb",
"tcr", "tcr",
"txt", "txt",
], ],
}, },
to: { to: {
document: [ document: [
"azw3", "azw3",
"docx", "docx",
"epub", "epub",
"fb2", "fb2",
"html", "html",
"htmlz", "htmlz",
"kepub.epub", "kepub.epub",
"lit", "lit",
"lrf", "lrf",
"mobi", "mobi",
"oeb", "oeb",
"pdb", "pdb",
"pdf", "pdf",
"pml", "pml",
"rb", "rb",
"rtf", "rtf",
"snb", "snb",
"tcr", "tcr",
"txt", "txt",
"txtz", "txtz",
], ],
}, },
}; };
export async function convert( export async function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => { execFile("ebook-convert", [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,48 +1,48 @@
import fs from "fs"; import fs from "fs";
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
document: ["yaml", "toml", "json", "xml", "csv"], document: ["yaml", "toml", "json", "xml", "csv"],
}, },
to: { to: {
document: ["yaml", "toml", "json", "csv"], document: ["yaml", "toml", "json", "csv"],
}, },
}; };
export async function convert( export async function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
const args: string[] = []; const args: string[] = [];
args.push("--file", filePath); args.push("--file", filePath);
args.push("--read", fileType); args.push("--read", fileType);
args.push("--write", convertTo); args.push("--write", convertTo);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("dasel", args, (error, stdout, stderr) => { execFile("dasel", args, (error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);
return; return;
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
fs.writeFile(targetPath, stdout, (err: NodeJS.ErrnoException | null) => { fs.writeFile(targetPath, stdout, (err: NodeJS.ErrnoException | null) => {
if (err) { if (err) {
reject(`Failed to write output: ${err}`); reject(`Failed to write output: ${err}`);
} else { } else {
resolve("Done"); resolve("Done");
} }
}); });
}); });
}); });
} }

View File

@@ -1,49 +1,49 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
images: ["dvi", "xdv", "pdf", "eps"], images: ["dvi", "xdv", "pdf", "eps"],
}, },
to: { to: {
images: ["svg", "svgz"], images: ["svg", "svgz"],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
const inputArgs: string[] = []; const inputArgs: string[] = [];
if (fileType === "eps") { if (fileType === "eps") {
inputArgs.push("--eps"); inputArgs.push("--eps");
} }
if (fileType === "pdf") { if (fileType === "pdf") {
inputArgs.push("--pdf"); inputArgs.push("--pdf");
} }
if (convertTo === "svgz") { if (convertTo === "svgz") {
inputArgs.push("-z"); inputArgs.push("-z");
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("dvisvgm", [...inputArgs, filePath, "-o", targetPath], (error, stdout, stderr) => { execFile("dvisvgm", [...inputArgs, filePath, "-o", 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");
}); });
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,337 +1,337 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
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,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("gm", ["convert", filePath, targetPath], (error, stdout, stderr) => { execFile("gm", ["convert", 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,492 +1,492 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
// declare possible conversions // declare possible conversions
export const properties = { export const properties = {
from: { from: {
images: [ images: [
"3fr", "3fr",
"3g2", "3g2",
"3gp", "3gp",
"aai", "aai",
"ai", "ai",
"apng", "apng",
"art", "art",
"arw", "arw",
"avci", "avci",
"avi", "avi",
"avif", "avif",
"avs", "avs",
"bayer", "bayer",
"bayera", "bayera",
"bgr", "bgr",
"bgra", "bgra",
"bgro", "bgro",
"bmp", "bmp",
"bmp2", "bmp2",
"bmp3", "bmp3",
"cal", "cal",
"cals", "cals",
"canvas", "canvas",
"caption", "caption",
"cin", "cin",
"clip", "clip",
"clipboard", "clipboard",
"cmyk", "cmyk",
"cmyka", "cmyka",
"cr2", "cr2",
"cr3", "cr3",
"crw", "crw",
"cube", "cube",
"cur", "cur",
"cut", "cut",
"data", "data",
"dcm", "dcm",
"dcr", "dcr",
"dcraw", "dcraw",
"dcx", "dcx",
"dds", "dds",
"dfont", "dfont",
"dng", "dng",
"dpx", "dpx",
"dxt1", "dxt1",
"dxt5", "dxt5",
"emf", "emf",
"epdf", "epdf",
"epi", "epi",
"eps", "eps",
"epsf", "epsf",
"epsi", "epsi",
"ept", "ept",
"ept2", "ept2",
"ept3", "ept3",
"erf", "erf",
"exr", "exr",
"farbfeld", "farbfeld",
"fax", "fax",
"ff", "ff",
"fff", "fff",
"file", "file",
"fits", "fits",
"fl32", "fl32",
"flif", "flif",
"flv", "flv",
"fractal", "fractal",
"ftp", "ftp",
"fts", "fts",
"ftxt", "ftxt",
"g3", "g3",
"g4", "g4",
"gif", "gif",
"gif87", "gif87",
"gradient", "gradient",
"gray", "gray",
"graya", "graya",
"group4", "group4",
"hald", "hald",
"hdr", "hdr",
"heic", "heic",
"heif", "heif",
"hrz", "hrz",
"http", "http",
"https", "https",
"icb", "icb",
"ico", "ico",
"icon", "icon",
"iiq", "iiq",
"inline", "inline",
"ipl", "ipl",
"j2c", "j2c",
"j2k", "j2k",
"jng", "jng",
"jnx", "jnx",
"jp2", "jp2",
"jpc", "jpc",
"jpe", "jpe",
"jpeg", "jpeg",
"jpg", "jpg",
"jpm", "jpm",
"jps", "jps",
"jpt", "jpt",
"jxl", "jxl",
"k25", "k25",
"kdc", "kdc",
"label", "label",
"m2v", "m2v",
"m4v", "m4v",
"mac", "mac",
"map", "map",
"mask", "mask",
"mat", "mat",
"mdc", "mdc",
"mef", "mef",
"miff", "miff",
"mkv", "mkv",
"mng", "mng",
"mono", "mono",
"mos", "mos",
"mov", "mov",
"mp4", "mp4",
"mpc", "mpc",
"mpeg", "mpeg",
"mpg", "mpg",
"mpo", "mpo",
"mrw", "mrw",
"msl", "msl",
"msvg", "msvg",
"mtv", "mtv",
"mvg", "mvg",
"nef", "nef",
"nrw", "nrw",
"null", "null",
"ora", "ora",
"orf", "orf",
"otb", "otb",
"otf", "otf",
"pal", "pal",
"palm", "palm",
"pam", "pam",
"pango", "pango",
"pattern", "pattern",
"pbm", "pbm",
"pcd", "pcd",
"pcds", "pcds",
"pcl", "pcl",
"pct", "pct",
"pcx", "pcx",
"pdb", "pdb",
"pdf", "pdf",
"pdfa", "pdfa",
"pef", "pef",
"pes", "pes",
"pfa", "pfa",
"pfb", "pfb",
"pfm", "pfm",
"pgm", "pgm",
"pgx", "pgx",
"phm", "phm",
"picon", "picon",
"pict", "pict",
"pix", "pix",
"pjpeg", "pjpeg",
"plasma", "plasma",
"png", "png",
"png00", "png00",
"png24", "png24",
"png32", "png32",
"png48", "png48",
"png64", "png64",
"png8", "png8",
"pnm", "pnm",
"pocketmod", "pocketmod",
"ppm", "ppm",
"ps", "ps",
"psb", "psb",
"psd", "psd",
"ptif", "ptif",
"pwp", "pwp",
"qoi", "qoi",
"radial", "radial",
"raf", "raf",
"ras", "ras",
"raw", "raw",
"rgb", "rgb",
"rgb565", "rgb565",
"rgba", "rgba",
"rgbo", "rgbo",
"rgf", "rgf",
"rla", "rla",
"rle", "rle",
"rmf", "rmf",
"rsvg", "rsvg",
"rw2", "rw2",
"rwl", "rwl",
"scr", "scr",
"screenshot", "screenshot",
"sct", "sct",
"sfw", "sfw",
"sgi", "sgi",
"six", "six",
"sixel", "sixel",
"sr2", "sr2",
"srf", "srf",
"srw", "srw",
"stegano", "stegano",
"sti", "sti",
"strimg", "strimg",
"sun", "sun",
"svg", "svg",
"svgz", "svgz",
"text", "text",
"tga", "tga",
"tiff", "tiff",
"tiff64", "tiff64",
"tile", "tile",
"tim", "tim",
"tm2", "tm2",
"ttc", "ttc",
"ttf", "ttf",
"txt", "txt",
"uyvy", "uyvy",
"vda", "vda",
"vicar", "vicar",
"vid", "vid",
"viff", "viff",
"vips", "vips",
"vst", "vst",
"wbmp", "wbmp",
"webm", "webm",
"webp", "webp",
"wmf", "wmf",
"wmv", "wmv",
"wpg", "wpg",
"x3f", "x3f",
"xbm", "xbm",
"xc", "xc",
"xcf", "xcf",
"xpm", "xpm",
"xps", "xps",
"xv", "xv",
"ycbcr", "ycbcr",
"ycbcra", "ycbcra",
"yuv", "yuv",
], ],
}, },
to: { to: {
images: [ images: [
"aai", "aai",
"ai", "ai",
"apng", "apng",
"art", "art",
"ashlar", "ashlar",
"avif", "avif",
"avs", "avs",
"bayer", "bayer",
"bayera", "bayera",
"bgr", "bgr",
"bgra", "bgra",
"bgro", "bgro",
"bmp", "bmp",
"bmp2", "bmp2",
"bmp3", "bmp3",
"brf", "brf",
"cal", "cal",
"cals", "cals",
"cin", "cin",
"cip", "cip",
"clip", "clip",
"clipboard", "clipboard",
"cmyk", "cmyk",
"cmyka", "cmyka",
"cur", "cur",
"data", "data",
"dcx", "dcx",
"dds", "dds",
"dpx", "dpx",
"dxt1", "dxt1",
"dxt5", "dxt5",
"epdf", "epdf",
"epi", "epi",
"eps", "eps",
"eps2", "eps2",
"eps3", "eps3",
"epsf", "epsf",
"epsi", "epsi",
"ept", "ept",
"ept2", "ept2",
"ept3", "ept3",
"exr", "exr",
"farbfeld", "farbfeld",
"fax", "fax",
"ff", "ff",
"fits", "fits",
"fl32", "fl32",
"flif", "flif",
"flv", "flv",
"fts", "fts",
"ftxt", "ftxt",
"g3", "g3",
"g4", "g4",
"gif", "gif",
"gif87", "gif87",
"gray", "gray",
"graya", "graya",
"group4", "group4",
"hdr", "hdr",
"histogram", "histogram",
"hrz", "hrz",
"htm", "htm",
"html", "html",
"icb", "icb",
"ico", "ico",
"icon", "icon",
"info", "info",
"inline", "inline",
"ipl", "ipl",
"isobrl", "isobrl",
"isobrl6", "isobrl6",
"j2c", "j2c",
"j2k", "j2k",
"jng", "jng",
"jp2", "jp2",
"jpc", "jpc",
"jpe", "jpe",
"jpeg", "jpeg",
"jpg", "jpg",
"jpm", "jpm",
"jps", "jps",
"jpt", "jpt",
"json", "json",
"jxl", "jxl",
"m2v", "m2v",
"m4v", "m4v",
"map", "map",
"mask", "mask",
"mat", "mat",
"matte", "matte",
"miff", "miff",
"mkv", "mkv",
"mng", "mng",
"mono", "mono",
"mov", "mov",
"mp4", "mp4",
"mpc", "mpc",
"mpeg", "mpeg",
"mpg", "mpg",
"msl", "msl",
"msvg", "msvg",
"mtv", "mtv",
"mvg", "mvg",
"null", "null",
"otb", "otb",
"pal", "pal",
"palm", "palm",
"pam", "pam",
"pbm", "pbm",
"pcd", "pcd",
"pcds", "pcds",
"pcl", "pcl",
"pct", "pct",
"pcx", "pcx",
"pdb", "pdb",
"pdf", "pdf",
"pdfa", "pdfa",
"pfm", "pfm",
"pgm", "pgm",
"pgx", "pgx",
"phm", "phm",
"picon", "picon",
"pict", "pict",
"pjpeg", "pjpeg",
"png", "png",
"png00", "png00",
"png24", "png24",
"png32", "png32",
"png48", "png48",
"png64", "png64",
"png8", "png8",
"pnm", "pnm",
"pocketmod", "pocketmod",
"ppm", "ppm",
"ps", "ps",
"ps2", "ps2",
"ps3", "ps3",
"psb", "psb",
"psd", "psd",
"ptif", "ptif",
"qoi", "qoi",
"ras", "ras",
"rgb", "rgb",
"rgba", "rgba",
"rgbo", "rgbo",
"rgf", "rgf",
"rsvg", "rsvg",
"sgi", "sgi",
"shtml", "shtml",
"six", "six",
"sixel", "sixel",
"sparse", "sparse",
"strimg", "strimg",
"sun", "sun",
"svg", "svg",
"svgz", "svgz",
"tga", "tga",
"thumbnail", "thumbnail",
"tiff", "tiff",
"tiff64", "tiff64",
"txt", "txt",
"ubrl", "ubrl",
"ubrl6", "ubrl6",
"uil", "uil",
"uyvy", "uyvy",
"vda", "vda",
"vicar", "vicar",
"vid", "vid",
"viff", "viff",
"vips", "vips",
"vst", "vst",
"wbmp", "wbmp",
"webm", "webm",
"webp", "webp",
"wmv", "wmv",
"wpg", "wpg",
"xbm", "xbm",
"xpm", "xpm",
"xv", "xv",
"yaml", "yaml",
"ycbcr", "ycbcr",
"ycbcra", "ycbcra",
"yuv", "yuv",
], ],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
let outputArgs: string[] = []; let outputArgs: string[] = [];
let inputArgs: string[] = []; let inputArgs: string[] = [];
if (convertTo === "ico") { if (convertTo === "ico") {
outputArgs = ["-define", "icon:auto-resize=256,128,64,48,32,16", "-background", "none"]; outputArgs = ["-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
inputArgs = ["-background", "none", "-density", "512"]; inputArgs = ["-background", "none", "-density", "512"];
} }
} }
// Handle EMF files specifically to avoid LibreOffice delegate issues // Handle EMF files specifically to avoid LibreOffice delegate issues
if (fileType === "emf") { if (fileType === "emf") {
// Use direct conversion without delegates for EMF files // Use direct conversion without delegates for EMF files
inputArgs.push("-define", "emf:delegate=false", "-density", "300"); inputArgs.push("-define", "emf:delegate=false", "-density", "300");
outputArgs.push("-background", "white", "-alpha", "remove"); outputArgs.push("-background", "white", "-alpha", "remove");
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile( execFile(
"magick", "magick",
[...inputArgs, filePath, ...outputArgs, targetPath], [...inputArgs, filePath, ...outputArgs, targetPath],
(error, stdout, stderr) => { (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,56 +1,56 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
images: ["svg", "pdf", "eps", "ps", "wmf", "emf", "png"], images: ["svg", "pdf", "eps", "ps", "wmf", "emf", "png"],
}, },
to: { to: {
images: [ images: [
"dxf", "dxf",
"emf", "emf",
"eps", "eps",
"fxg", "fxg",
"gpl", "gpl",
"hpgl", "hpgl",
"html", "html",
"odg", "odg",
"pdf", "pdf",
"png", "png",
"pov", "pov",
"ps", "ps",
"sif", "sif",
"svg", "svg",
"svgz", "svgz",
"tex", "tex",
"wmf", "wmf",
], ],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => { execFile("inkscape", [filePath, "-o", 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,38 +1,38 @@
import { execFile as execFileOriginal } from "child_process"; import { execFile as execFileOriginal } from "child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
images: ["avci", "avcs", "avif", "h264", "heic", "heics", "heif", "heifs", "hif", "mkv", "mp4"], images: ["avci", "avcs", "avif", "h264", "heic", "heics", "heif", "heifs", "hif", "mkv", "mp4"],
}, },
to: { to: {
images: ["jpeg", "png", "y4m"], images: ["jpeg", "png", "y4m"],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => { execFile("heif-convert", [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,50 +1,50 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
// declare possible conversions // declare possible conversions
export const properties = { export const properties = {
from: { from: {
jxl: ["jxl"], jxl: ["jxl"],
images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"], images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
}, },
to: { to: {
jxl: ["apng", "exr", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"], jxl: ["apng", "exr", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
images: ["jxl"], images: ["jxl"],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
let tool = ""; let tool = "";
if (fileType === "jxl") { if (fileType === "jxl") {
tool = "djxl"; tool = "djxl";
} }
if (convertTo === "jxl") { if (convertTo === "jxl") {
tool = "cjxl"; tool = "cjxl";
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile(tool, [filePath, targetPath], (error, stdout, stderr) => { execFile(tool, [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,177 +1,177 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
text: [ text: [
"602", "602",
"abw", "abw",
"csv", "csv",
"cwk", "cwk",
"doc", "doc",
"docm", "docm",
"docx", "docx",
"dot", "dot",
"dotx", "dotx",
"dotm", "dotm",
"epub", "epub",
"fb2", "fb2",
"fodt", "fodt",
"htm", "htm",
"html", "html",
"hwp", "hwp",
"mcw", "mcw",
"mw", "mw",
"mwd", "mwd",
"lwp", "lwp",
"lrf", "lrf",
"odt", "odt",
"ott", "ott",
"pages", "pages",
"pdf", "pdf",
"psw", "psw",
"rtf", "rtf",
"sdw", "sdw",
"stw", "stw",
"sxw", "sxw",
"tab", "tab",
"tsv", "tsv",
"txt", "txt",
"wn", "wn",
"wpd", "wpd",
"wps", "wps",
"wpt", "wpt",
"wri", "wri",
"xhtml", "xhtml",
"xml", "xml",
"zabw", "zabw",
], ],
}, },
to: { to: {
text: [ text: [
"csv", "csv",
"doc", "doc",
"docm", "docm",
"docx", "docx",
"dot", "dot",
"dotx", "dotx",
"dotm", "dotm",
"epub", "epub",
"fodt", "fodt",
"htm", "htm",
"html", "html",
"odt", "odt",
"ott", "ott",
"pdf", "pdf",
"rtf", "rtf",
"tab", "tab",
"tsv", "tsv",
"txt", "txt",
"wps", "wps",
"wpt", "wpt",
"xhtml", "xhtml",
"xml", "xml",
], ],
}, },
}; };
type FileCategories = "text" | "calc"; type FileCategories = "text" | "calc";
const filters: Record<FileCategories, Record<string, string>> = { const filters: Record<FileCategories, Record<string, string>> = {
text: { text: {
"602": "T602Document", "602": "T602Document",
abw: "AbiWord", abw: "AbiWord",
csv: "Text", csv: "Text",
doc: "MS Word 97", doc: "MS Word 97",
docm: "MS Word 2007 XML VBA", docm: "MS Word 2007 XML VBA",
docx: "MS Word 2007 XML", docx: "MS Word 2007 XML",
dot: "MS Word 97 Vorlage", dot: "MS Word 97 Vorlage",
dotx: "MS Word 2007 XML Template", dotx: "MS Word 2007 XML Template",
dotm: "MS Word 2007 XML Template", dotm: "MS Word 2007 XML Template",
epub: "EPUB", epub: "EPUB",
fb2: "Fictionbook 2", fb2: "Fictionbook 2",
fodt: "OpenDocument Text Flat XML", fodt: "OpenDocument Text Flat XML",
htm: "HTML (StarWriter)", htm: "HTML (StarWriter)",
html: "HTML (StarWriter)", html: "HTML (StarWriter)",
hwp: "writer_MIZI_Hwp_97", hwp: "writer_MIZI_Hwp_97",
mcw: "MacWrite", mcw: "MacWrite",
mw: "MacWrite", mw: "MacWrite",
mwd: "Mariner_Write", mwd: "Mariner_Write",
lwp: "LotusWordPro", lwp: "LotusWordPro",
lrf: "BroadBand eBook", lrf: "BroadBand eBook",
odt: "writer8", odt: "writer8",
ott: "writer8_template", ott: "writer8_template",
pages: "Apple Pages", pages: "Apple Pages",
// pdf: "writer_pdf_import", // pdf: "writer_pdf_import",
psw: "PocketWord File", psw: "PocketWord File",
rtf: "Rich Text Format", rtf: "Rich Text Format",
sdw: "StarOffice_Writer", sdw: "StarOffice_Writer",
stw: "writer_StarOffice_XML_Writer_Template", stw: "writer_StarOffice_XML_Writer_Template",
sxw: "StarOffice XML (Writer)", sxw: "StarOffice XML (Writer)",
tab: "Text", tab: "Text",
tsv: "Text", tsv: "Text",
txt: "Text", txt: "Text",
wn: "WriteNow", wn: "WriteNow",
wpd: "WordPerfect", wpd: "WordPerfect",
wps: "MS Word 97", wps: "MS Word 97",
wpt: "MS Word 97 Vorlage", wpt: "MS Word 97 Vorlage",
wri: "MS_Write", wri: "MS_Write",
xhtml: "HTML (StarWriter)", xhtml: "HTML (StarWriter)",
xml: "OpenDocument Text Flat XML", xml: "OpenDocument Text Flat XML",
zabw: "AbiWord", zabw: "AbiWord",
}, },
calc: {}, calc: {},
}; };
const getFilters = (fileType: string, converto: string) => { const getFilters = (fileType: string, converto: string) => {
if (fileType in filters.text && converto in filters.text) { if (fileType in filters.text && converto in filters.text) {
return [filters.text[fileType], filters.text[converto]]; return [filters.text[fileType], filters.text[converto]];
} else if (fileType in filters.calc && converto in filters.calc) { } else if (fileType in filters.calc && converto in filters.calc) {
return [filters.calc[fileType], filters.calc[converto]]; return [filters.calc[fileType], filters.calc[converto]];
} }
return [null, null]; return [null, null];
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, execFile: ExecFileFn = execFileOriginal,
): Promise<string> { ): Promise<string> {
const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "") ?? targetPath; const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "") ?? targetPath;
// Build arguments array // Build arguments array
const args: string[] = []; const args: string[] = [];
args.push("--headless"); args.push("--headless");
const [inFilter, outFilter] = getFilters(fileType, convertTo); const [inFilter, outFilter] = getFilters(fileType, convertTo);
if (inFilter) { if (inFilter) {
args.push(`--infilter="${inFilter}"`); args.push(`--infilter="${inFilter}"`);
} }
if (outFilter) { if (outFilter) {
args.push("--convert-to", `${convertTo}:${outFilter}`, "--outdir", outputPath, filePath); args.push("--convert-to", `${convertTo}:${outFilter}`, "--outdir", outputPath, filePath);
} else { } else {
args.push("--convert-to", convertTo, "--outdir", outputPath, filePath); args.push("--convert-to", convertTo, "--outdir", outputPath, filePath);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile("soffice", args, (error, stdout, stderr) => { execFile("soffice", 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

@@ -1,364 +1,364 @@
import { Cookie } from "elysia"; import { Cookie } from "elysia";
import db from "../db/db"; import db from "../db/db";
import { MAX_CONVERT_PROCESS } from "../helpers/env"; import { MAX_CONVERT_PROCESS } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype"; import { normalizeFiletype, normalizeOutputFiletype } 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 convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertDasel, properties as propertiesDasel } from "./dasel"; import { convert as convertDasel, properties as propertiesDasel } from "./dasel";
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm"; 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 { import {
convert as convertGraphicsmagick, convert as convertGraphicsmagick,
properties as propertiesGraphicsmagick, properties as propertiesGraphicsmagick,
} from "./graphicsmagick"; } from "./graphicsmagick";
import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick"; 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 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 convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice"; import { convert as convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice";
import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert"; import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert";
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 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 convertVtracer, properties as propertiesVtracer } from "./vtracer"; import { convert as convertVtracer, properties as propertiesVtracer } from "./vtracer";
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex"; import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
// 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
const properties: Record< const properties: Record<
string, string,
{ {
properties: { properties: {
from: Record<string, string[]>; from: Record<string, string[]>;
to: Record<string, string[]>; to: Record<string, string[]>;
options?: Record< options?: Record<
string, string,
Record< Record<
string, string,
{ {
description: string; description: string;
type: string; type: string;
default: number; default: number;
} }
> >
>; >;
}; };
converter: ( converter: (
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
) => unknown; ) => unknown;
} }
> = { > = {
// Prioritize Inkscape for EMF files as it handles them better than ImageMagick // Prioritize Inkscape for EMF files as it handles them better than ImageMagick
inkscape: { inkscape: {
properties: propertiesInkscape, properties: propertiesInkscape,
converter: convertInkscape, converter: convertInkscape,
}, },
libjxl: { libjxl: {
properties: propertiesLibjxl, properties: propertiesLibjxl,
converter: convertLibjxl, converter: convertLibjxl,
}, },
resvg: { resvg: {
properties: propertiesresvg, properties: propertiesresvg,
converter: convertresvg, converter: convertresvg,
}, },
vips: { vips: {
properties: propertiesImage, properties: propertiesImage,
converter: convertImage, converter: convertImage,
}, },
libheif: { libheif: {
properties: propertiesLibheif, properties: propertiesLibheif,
converter: convertLibheif, converter: convertLibheif,
}, },
xelatex: { xelatex: {
properties: propertiesxelatex, properties: propertiesxelatex,
converter: convertxelatex, converter: convertxelatex,
}, },
calibre: { calibre: {
properties: propertiesCalibre, properties: propertiesCalibre,
converter: convertCalibre, converter: convertCalibre,
}, },
dasel: { dasel: {
properties: propertiesDasel, properties: propertiesDasel,
converter: convertDasel, converter: convertDasel,
}, },
libreoffice: { libreoffice: {
properties: propertiesLibreOffice, properties: propertiesLibreOffice,
converter: convertLibreOffice, converter: convertLibreOffice,
}, },
pandoc: { pandoc: {
properties: propertiesPandoc, properties: propertiesPandoc,
converter: convertPandoc, converter: convertPandoc,
}, },
msgconvert: { msgconvert: {
properties: propertiesMsgconvert, properties: propertiesMsgconvert,
converter: convertMsgconvert, converter: convertMsgconvert,
}, },
dvisvgm: { dvisvgm: {
properties: propertiesDvisvgm, properties: propertiesDvisvgm,
converter: convertDvisvgm, converter: convertDvisvgm,
}, },
imagemagick: { imagemagick: {
properties: propertiesImagemagick, properties: propertiesImagemagick,
converter: convertImagemagick, converter: convertImagemagick,
}, },
graphicsmagick: { graphicsmagick: {
properties: propertiesGraphicsmagick, properties: propertiesGraphicsmagick,
converter: convertGraphicsmagick, converter: convertGraphicsmagick,
}, },
assimp: { assimp: {
properties: propertiesassimp, properties: propertiesassimp,
converter: convertassimp, converter: convertassimp,
}, },
ffmpeg: { ffmpeg: {
properties: propertiesFFmpeg, properties: propertiesFFmpeg,
converter: convertFFmpeg, converter: convertFFmpeg,
}, },
potrace: { potrace: {
properties: propertiesPotrace, properties: propertiesPotrace,
converter: convertPotrace, converter: convertPotrace,
}, },
vtracer: { vtracer: {
properties: propertiesVtracer, properties: propertiesVtracer,
converter: convertVtracer, converter: convertVtracer,
}, },
}; };
function chunks<T>(arr: T[], size: number): T[][] { function chunks<T>(arr: T[], size: number): T[][] {
if (size <= 0) { if (size <= 0) {
return [arr]; return [arr];
} }
return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) => return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) =>
arr.slice(i * size, i * size + size), arr.slice(i * size, i * size + size),
); );
} }
export async function handleConvert( export async function handleConvert(
fileNames: string[], fileNames: string[],
userUploadsDir: string, userUploadsDir: string,
userOutputDir: string, userOutputDir: string,
convertTo: string, convertTo: string,
converterName: string, converterName: string,
jobId: Cookie<string | undefined>, jobId: Cookie<string | undefined>,
) { ) {
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)",
); );
for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) { for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) {
const toProcess: Promise<string>[] = []; const toProcess: Promise<string>[] = [];
for (const fileName of chunk) { for (const fileName of chunk) {
const filePath = `${userUploadsDir}${fileName}`; const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop() ?? ""; const fileTypeOrig = fileName.split(".").pop() ?? "";
const fileType = normalizeFiletype(fileTypeOrig); const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo); const newFileExt = normalizeOutputFiletype(convertTo);
const newFileName = fileName.replace( const newFileName = fileName.replace(
new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`), new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
newFileExt, newFileExt,
); );
const targetPath = `${userOutputDir}${newFileName}`; const targetPath = `${userOutputDir}${newFileName}`;
toProcess.push( toProcess.push(
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName) mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
.then((r) => { .then((r) => {
if (jobId.value) { if (jobId.value) {
query.run(jobId.value, fileName, newFileName, r); query.run(jobId.value, fileName, newFileName, r);
} }
resolve(r); resolve(r);
}) })
.catch((c) => reject(c)); .catch((c) => reject(c));
}), }),
); );
} }
await Promise.all(toProcess); await Promise.all(toProcess);
} }
} }
async function mainConverter( async function mainConverter(
inputFilePath: string, inputFilePath: string,
fileTypeOriginal: string, fileTypeOriginal: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
converterName?: string, converterName?: string,
) { ) {
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;
} else { } else {
// Iterate over each converter in properties // Iterate over each converter in properties
for (converterName in properties) { for (converterName in properties) {
const converterObj = properties[converterName]; const converterObj = properties[converterName];
if (!converterObj) { if (!converterObj) {
break; break;
} }
for (const key in converterObj.properties.from) { for (const key in converterObj.properties.from) {
if ( if (
converterObj?.properties?.from[key]?.includes(fileType) && converterObj?.properties?.from[key]?.includes(fileType) &&
converterObj?.properties?.to[key]?.includes(convertTo) converterObj?.properties?.to[key]?.includes(convertTo)
) { ) {
converterFunc = converterObj.converter; converterFunc = converterObj.converter;
break; break;
} }
} }
} }
} }
if (!converterFunc) { if (!converterFunc) {
console.log(`No available converter supports converting from ${fileType} to ${convertTo}.`); console.log(`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(inputFilePath, fileType, convertTo, targetPath, options); const result = await converterFunc(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}.`,
result, result,
); );
if (typeof result === "string") { if (typeof result === "string") {
return result; return result;
} }
return "Done"; return "Done";
} catch (error) { } catch (error) {
console.error( console.error(
`Failed to convert ${inputFilePath} from ${fileType} to ${convertTo} using ${converterName}.`, `Failed to convert ${inputFilePath} from ${fileType} to ${convertTo} using ${converterName}.`,
error, error,
); );
return "Failed, check logs"; return "Failed, check logs";
} }
} }
const possibleTargets: Record<string, Record<string, string[]>> = {}; const possibleTargets: Record<string, Record<string, string[]>> = {};
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;
if (!converterProperties) { if (!converterProperties) {
continue; continue;
} }
for (const key in converterProperties.from) { for (const key in converterProperties.from) {
if (converterProperties.from[key] === undefined) { if (converterProperties.from[key] === undefined) {
continue; continue;
} }
for (const extension of converterProperties.from[key] ?? []) { for (const extension of converterProperties.from[key] ?? []) {
if (!possibleTargets[extension]) { if (!possibleTargets[extension]) {
possibleTargets[extension] = {}; possibleTargets[extension] = {};
} }
possibleTargets[extension][converterName] = converterProperties.to[key] || []; possibleTargets[extension][converterName] = converterProperties.to[key] || [];
} }
} }
} }
export const getPossibleTargets = (from: string): Record<string, string[]> => { export const getPossibleTargets = (from: string): Record<string, string[]> => {
const fromClean = normalizeFiletype(from); const fromClean = normalizeFiletype(from);
return possibleTargets[fromClean] || {}; return possibleTargets[fromClean] || {};
}; };
const possibleInputs: string[] = []; const possibleInputs: string[] = [];
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;
if (!converterProperties) { if (!converterProperties) {
continue; continue;
} }
for (const key in converterProperties.from) { for (const key in converterProperties.from) {
for (const extension of converterProperties.from[key] ?? []) { for (const extension of converterProperties.from[key] ?? []) {
if (!possibleInputs.includes(extension)) { if (!possibleInputs.includes(extension)) {
possibleInputs.push(extension); possibleInputs.push(extension);
} }
} }
} }
} }
possibleInputs.sort(); possibleInputs.sort();
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const getPossibleInputs = () => { const getPossibleInputs = () => {
return possibleInputs; return possibleInputs;
}; };
const allTargets: Record<string, string[]> = {}; const allTargets: Record<string, string[]> = {};
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;
if (!converterProperties) { if (!converterProperties) {
continue; continue;
} }
for (const key in converterProperties.to) { for (const key in converterProperties.to) {
if (allTargets[converterName]) { if (allTargets[converterName]) {
allTargets[converterName].push(...(converterProperties.to[key] || [])); allTargets[converterName].push(...(converterProperties.to[key] || []));
} else { } else {
allTargets[converterName] = converterProperties.to[key] || []; allTargets[converterName] = converterProperties.to[key] || [];
} }
} }
} }
export const getAllTargets = () => { export const getAllTargets = () => {
return allTargets; return allTargets;
}; };
const allInputs: Record<string, string[]> = {}; const allInputs: Record<string, string[]> = {};
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;
if (!converterProperties) { if (!converterProperties) {
continue; continue;
} }
for (const key in converterProperties.from) { for (const key in converterProperties.from) {
if (allInputs[converterName]) { if (allInputs[converterName]) {
allInputs[converterName].push(...(converterProperties.from[key] || [])); allInputs[converterName].push(...(converterProperties.from[key] || []));
} else { } else {
allInputs[converterName] = converterProperties.from[key] || []; allInputs[converterName] = converterProperties.from[key] || [];
} }
} }
} }
export const getAllInputs = (converter: string) => { export const getAllInputs = (converter: string) => {
return allInputs[converter] || []; return allInputs[converter] || [];
}; };
// // count the number of unique formats // // count the number of unique formats
// const uniqueFormats = new Set(); // const uniqueFormats = new Set();
// for (const converterName in properties) { // for (const converterName in properties) {
// const converterProperties = properties[converterName]?.properties; // const converterProperties = properties[converterName]?.properties;
// if (!converterProperties) { // if (!converterProperties) {
// continue; // continue;
// } // }
// for (const key in converterProperties.from) { // for (const key in converterProperties.from) {
// for (const extension of converterProperties.from[key] ?? []) { // for (const extension of converterProperties.from[key] ?? []) {
// uniqueFormats.add(extension); // uniqueFormats.add(extension);
// } // }
// } // }
// for (const key in converterProperties.to) { // for (const key in converterProperties.to) {
// for (const extension of converterProperties.to[key] ?? []) { // for (const extension of converterProperties.to[key] ?? []) {
// uniqueFormats.add(extension); // uniqueFormats.add(extension);
// } // }
// } // }
// } // }
// // 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,52 +1,52 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
email: ["msg"], email: ["msg"],
}, },
to: { to: {
email: ["eml"], email: ["eml"],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, execFile: ExecFileFn = execFileOriginal,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (fileType === "msg" && convertTo === "eml") { if (fileType === "msg" && convertTo === "eml") {
// Convert MSG to EML using msgconvert // Convert MSG to EML using msgconvert
// msgconvert will output to the same directory as the input file with .eml extension // msgconvert will output to the same directory as the input file with .eml extension
// We need to use --outfile to specify the target path // We need to use --outfile to specify the target path
const args = ["--outfile", targetPath, filePath]; const args = ["--outfile", targetPath, filePath];
execFile("msgconvert", args, (error, stdout, stderr) => { execFile("msgconvert", args, (error, stdout, stderr) => {
if (error) { if (error) {
reject(new Error(`msgconvert failed: ${error.message}`)); reject(new Error(`msgconvert failed: ${error.message}`));
return; return;
} }
if (stderr) { if (stderr) {
// Log sanitized stderr to avoid exposing sensitive paths // Log sanitized stderr to avoid exposing sensitive paths
const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]"); const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]");
console.warn( console.warn(
`msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`, `msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`,
); );
} }
resolve(targetPath); resolve(targetPath);
}); });
} else { } else {
reject( reject(
new Error( new Error(
`Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`, `Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`,
), ),
); );
} }
}); });
} }

View File

@@ -1,163 +1,163 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
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,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, execFile: ExecFileFn = execFileOriginal,
): 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

@@ -1,50 +1,50 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
images: ["pnm", "pbm", "pgm", "bmp"], images: ["pnm", "pbm", "pgm", "bmp"],
}, },
to: { to: {
images: [ images: [
"svg", "svg",
"pdf", "pdf",
"pdfpage", "pdfpage",
"eps", "eps",
"postscript", "postscript",
"ps", "ps",
"dxf", "dxf",
"geojson", "geojson",
"pgm", "pgm",
"gimppath", "gimppath",
"xfig", "xfig",
], ],
}, },
}; };
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): 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,38 +1,38 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
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,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): 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,17 +1,17 @@
import { ExecFileOptions } from "child_process"; import { ExecFileOptions } from "child_process";
export type ExecFileFn = ( export type ExecFileFn = (
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void, callback: (err: Error | null, stdout: string, stderr: string) => void,
options?: ExecFileOptions, options?: ExecFileOptions,
) => void; ) => void;
export type ConvertFnWithExecFile = ( export type ConvertFnWithExecFile = (
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options: unknown, options: unknown,
execFileOverride?: ExecFileFn, execFileOverride?: ExecFileFn,
) => Promise<string>; ) => Promise<string>;

View File

@@ -1,139 +1,139 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
// 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,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, execFile: ExecFileFn = execFileOriginal,
): 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("vips", [action, filePath, targetPath], (error, stdout, stderr) => { execFile("vips", [action, 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,80 +1,80 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
export const properties = { export const properties = {
from: { from: {
images: ["jpg", "jpeg", "png", "bmp", "gif", "tiff", "tif", "webp"], images: ["jpg", "jpeg", "png", "bmp", "gif", "tiff", "tif", "webp"],
}, },
to: { to: {
images: ["svg"], images: ["svg"],
}, },
}; };
interface VTracerOptions { interface VTracerOptions {
colormode?: string; colormode?: string;
hierarchical?: string; hierarchical?: string;
mode?: string; mode?: string;
filter_speckle?: string | number; filter_speckle?: string | number;
color_precision?: string | number; color_precision?: string | number;
layer_difference?: string | number; layer_difference?: string | number;
corner_threshold?: string | number; corner_threshold?: string | number;
length_threshold?: string | number; length_threshold?: string | number;
max_iterations?: string | number; max_iterations?: string | number;
splice_threshold?: string | number; splice_threshold?: string | number;
path_precision?: string | number; path_precision?: string | number;
} }
export function convert( export function convert(
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Build vtracer arguments // Build vtracer arguments
const args = ["--input", filePath, "--output", targetPath]; const args = ["--input", filePath, "--output", targetPath];
// Add optional parameter if provided // Add optional parameter if provided
if (options && typeof options === "object") { if (options && typeof options === "object") {
const opts = options as VTracerOptions; const opts = options as VTracerOptions;
const validOptions: Array<keyof VTracerOptions> = [ const validOptions: Array<keyof VTracerOptions> = [
"colormode", "colormode",
"hierarchical", "hierarchical",
"mode", "mode",
"filter_speckle", "filter_speckle",
"color_precision", "color_precision",
"layer_difference", "layer_difference",
"corner_threshold", "corner_threshold",
"length_threshold", "length_threshold",
"max_iterations", "max_iterations",
"splice_threshold", "splice_threshold",
"path_precision", "path_precision",
]; ];
for (const option of validOptions) { for (const option of validOptions) {
if (opts[option] !== undefined) { if (opts[option] !== undefined) {
args.push(`--${option}`, String(opts[option])); args.push(`--${option}`, String(opts[option]));
} }
} }
} }
execFile("vtracer", args, (error, stdout, stderr) => { execFile("vtracer", args, (error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}${stderr ? `\nstderr: ${stderr}` : ""}`); reject(`error: ${error}${stderr ? `\nstderr: ${stderr}` : ""}`);
return; return;
} }
if (stdout) { if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
} }
if (stderr) { if (stderr) {
console.log(`stderr: ${stderr}`); console.log(`stderr: ${stderr}`);
} }
resolve("Done"); resolve("Done");
}); });
}); });
} }

View File

@@ -1,45 +1,45 @@
import { execFile as execFileOriginal } from "node:child_process"; import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
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,
options?: unknown, options?: unknown,
execFile: ExecFileFn = execFileOriginal, execFile: ExecFileFn = execFileOriginal,
): 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.split("/").slice(0, -1).join("/").replace("./", ""); const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "");
execFile( execFile(
"latexmk", "latexmk",
["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath], ["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath],
(error, stdout, stderr) => { (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,41 +1,41 @@
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()) {
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL, email TEXT NOT NULL,
password TEXT NOT NULL password TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS file_names ( CREATE TABLE IF NOT EXISTS file_names (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL, job_id INTEGER NOT NULL,
file_name TEXT NOT NULL, file_name TEXT NOT NULL,
output_file_name TEXT NOT NULL, output_file_name TEXT NOT NULL,
status TEXT DEFAULT 'not started', status TEXT DEFAULT 'not started',
FOREIGN KEY (job_id) REFERENCES jobs(id) FOREIGN KEY (job_id) REFERENCES jobs(id)
); );
CREATE TABLE IF NOT EXISTS jobs ( CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
date_created TEXT NOT NULL, date_created TEXT NOT NULL,
status TEXT DEFAULT 'not started', status TEXT DEFAULT 'not started',
num_files INTEGER DEFAULT 0, num_files INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) FOREIGN KEY (user_id) REFERENCES users(id)
); );
PRAGMA user_version = 1;`); PRAGMA user_version = 1;`);
} }
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version; const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
if (dbVersion === 0) { if (dbVersion === 0) {
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';"); db.exec("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.");
} }
// 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

@@ -1,23 +1,23 @@
export class Filename { export class Filename {
id!: number; id!: number;
job_id!: number; job_id!: number;
file_name!: string; file_name!: string;
output_file_name!: string; output_file_name!: string;
status!: string; status!: string;
} }
export class Jobs { export class Jobs {
finished_files!: number; finished_files!: number;
id!: number; id!: number;
user_id!: number; user_id!: number;
date_created!: string; date_created!: string;
status!: string; status!: string;
num_files!: number; num_files!: number;
files_detailed!: Filename[]; files_detailed!: Filename[];
} }
export class User { export class User {
id!: number; id!: number;
email!: string; email!: string;
password!: string; password!: string;
} }

View File

@@ -1,25 +1,25 @@
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 = process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false; export const HTTP_ALLOWED = 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;
export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS 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 = process.env.HIDE_HISTORY?.toLowerCase() === "true" || false; export const HIDE_HISTORY = process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
export const WEBROOT = process.env.WEBROOT ?? ""; export const WEBROOT = process.env.WEBROOT ?? "";
export const LANGUAGE = process.env.LANGUAGE?.toLowerCase() || "en"; export const LANGUAGE = process.env.LANGUAGE?.toLowerCase() || "en";
export const MAX_CONVERT_PROCESS = export const MAX_CONVERT_PROCESS =
process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0 process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0
? Number(process.env.MAX_CONVERT_PROCESS) ? Number(process.env.MAX_CONVERT_PROCESS)
: 0; : 0;
export const UNAUTHENTICATED_USER_SHARING = export const UNAUTHENTICATED_USER_SHARING =
process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false; process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false;

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

@@ -1,186 +1,186 @@
import { exec } from "node:child_process"; import { exec } from "node:child_process";
import { version } from "../../package.json"; import { version } from "../../package.json";
console.log(`ConvertX v${version}`); console.log(`ConvertX v${version}`);
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
exec("cat /etc/os-release", (error, stdout) => { exec("cat /etc/os-release", (error, stdout) => {
if (error) { if (error) {
console.error("Not running on docker, this is not supported."); console.error("Not running on docker, this is not supported.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split('PRETTY_NAME="')[1]?.split('"')[0]); console.log(stdout.split('PRETTY_NAME="')[1]?.split('"')[0]);
} }
}); });
exec("pandoc -v", (error, stdout) => { exec("pandoc -v", (error, stdout) => {
if (error) { if (error) {
console.error("Pandoc is not installed."); console.error("Pandoc is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("ffmpeg -version", (error, stdout) => { exec("ffmpeg -version", (error, stdout) => {
if (error) { if (error) {
console.error("FFmpeg is not installed."); console.error("FFmpeg is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("vips -v", (error, stdout) => { exec("vips -v", (error, stdout) => {
if (error) { if (error) {
console.error("Vips is not installed."); console.error("Vips is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("magick --version", (error, stdout) => { exec("magick --version", (error, stdout) => {
if (error) { if (error) {
console.error("ImageMagick is not installed."); console.error("ImageMagick is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]?.replace("Version: ", "")); console.log(stdout.split("\n")[0]?.replace("Version: ", ""));
} }
}); });
exec("gm version", (error, stdout) => { exec("gm version", (error, stdout) => {
if (error) { if (error) {
console.error("GraphicsMagick is not installed."); console.error("GraphicsMagick is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("inkscape --version", (error, stdout) => { exec("inkscape --version", (error, stdout) => {
if (error) { if (error) {
console.error("Inkscape is not installed."); console.error("Inkscape is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("djxl --version", (error, stdout) => { exec("djxl --version", (error, stdout) => {
if (error) { if (error) {
console.error("libjxl-tools is not installed."); console.error("libjxl-tools is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("dasel --version", (error, stdout) => { exec("dasel --version", (error, stdout) => {
if (error) { if (error) {
console.error("dasel is not installed."); console.error("dasel is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("xelatex -version", (error, stdout) => { exec("xelatex -version", (error, stdout) => {
if (error) { if (error) {
console.error("Tex Live with XeTeX is not installed."); console.error("Tex Live with XeTeX is not installed.");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("resvg -V", (error, stdout) => { exec("resvg -V", (error, stdout) => {
if (error) { if (error) {
console.error("resvg is not installed"); console.error("resvg is not installed");
} }
if (stdout) { if (stdout) {
console.log(`resvg v${stdout.split("\n")[0]}`); console.log(`resvg v${stdout.split("\n")[0]}`);
} }
}); });
exec("assimp version", (error, stdout) => { exec("assimp version", (error, stdout) => {
if (error) { if (error) {
console.error("assimp is not installed"); console.error("assimp is not installed");
} }
if (stdout) { if (stdout) {
console.log(`assimp ${stdout.split("\n")[5]}`); console.log(`assimp ${stdout.split("\n")[5]}`);
} }
}); });
exec("ebook-convert --version", (error, stdout) => { exec("ebook-convert --version", (error, stdout) => {
if (error) { if (error) {
console.error("ebook-convert (calibre) is not installed"); console.error("ebook-convert (calibre) is not installed");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("heif-info -v", (error, stdout) => { exec("heif-info -v", (error, stdout) => {
if (error) { if (error) {
console.error("libheif is not installed"); console.error("libheif is not installed");
} }
if (stdout) { if (stdout) {
console.log(`libheif v${stdout.split("\n")[0]}`); console.log(`libheif v${stdout.split("\n")[0]}`);
} }
}); });
exec("potrace -v", (error, stdout) => { exec("potrace -v", (error, stdout) => {
if (error) { if (error) {
console.error("potrace is not installed"); console.error("potrace is not installed");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("soffice --version", (error, stdout) => { exec("soffice --version", (error, stdout) => {
if (error) { if (error) {
console.error("libreoffice is not installed"); console.error("libreoffice is not installed");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("msgconvert --version", (error, stdout) => { exec("msgconvert --version", (error, stdout) => {
if (error) { if (error) {
console.error("msgconvert (libemail-outlook-message-perl) is not installed"); console.error("msgconvert (libemail-outlook-message-perl) is not installed");
} }
if (stdout) { if (stdout) {
console.log(stdout.split("\n")[0]); console.log(stdout.split("\n")[0]);
} }
}); });
exec("bun -v", (error, stdout) => { exec("bun -v", (error, stdout) => {
if (error) { if (error) {
console.error("Bun is not installed. wait what"); console.error("Bun is not installed. wait what");
} }
if (stdout) { if (stdout) {
console.log(`Bun v${stdout.split("\n")[0]}`); console.log(`Bun v${stdout.split("\n")[0]}`);
} }
}); });
} }

View File

@@ -1,15 +1,15 @@
import tailwind from "@tailwindcss/postcss"; import tailwind from "@tailwindcss/postcss";
import postcss from "postcss"; import postcss from "postcss";
export const generateTailwind = async () => { export const generateTailwind = async () => {
const result = await Bun.file("./src/main.css") const result = await Bun.file("./src/main.css")
.text() .text()
.then((sourceText) => { .then((sourceText) => {
return postcss([tailwind]).process(sourceText, { return postcss([tailwind]).process(sourceText, {
from: "./src/main.css", from: "./src/main.css",
to: "./public/generated.css", to: "./public/generated.css",
}); });
}); });
return result; return result;
}; };

View File

@@ -1,94 +1,94 @@
import { rmSync } from "node:fs"; 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 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 { AUTO_DELETE_EVERY_N_HOURS, WEBROOT } from "./helpers/env";
import { chooseConverter } from "./pages/chooseConverter"; import { chooseConverter } from "./pages/chooseConverter";
import { convert } from "./pages/convert"; import { convert } from "./pages/convert";
import { deleteFile } from "./pages/deleteFile"; import { deleteFile } from "./pages/deleteFile";
import { download } from "./pages/download"; import { download } from "./pages/download";
import { history } from "./pages/history"; import { history } from "./pages/history";
import { listConverters } from "./pages/listConverters"; import { listConverters } from "./pages/listConverters";
import { results } from "./pages/results"; import { results } from "./pages/results";
import { root } from "./pages/root"; import { root } from "./pages/root";
import { upload } from "./pages/upload"; import { upload } from "./pages/upload";
import { user } from "./pages/user"; import { user } from "./pages/user";
mkdir("./data", { recursive: true }).catch(console.error); mkdir("./data", { recursive: true }).catch(console.error);
export const uploadsDir = "./data/uploads/"; export const uploadsDir = "./data/uploads/";
export const outputDir = "./data/output/"; export const outputDir = "./data/output/";
const app = new Elysia({ const app = new Elysia({
serve: { serve: {
maxRequestBodySize: Number.MAX_SAFE_INTEGER, maxRequestBodySize: Number.MAX_SAFE_INTEGER,
}, },
prefix: WEBROOT, prefix: WEBROOT,
}) })
.use(html()) .use(html())
.use( .use(
staticPlugin({ staticPlugin({
assets: "public", assets: "public",
prefix: "", prefix: "",
}), }),
) )
.use(user) .use(user)
.use(root) .use(root)
.use(upload) .use(upload)
.use(history) .use(history)
.use(convert) .use(convert)
.use(download) .use(download)
.use(results) .use(results)
.use(deleteFile) .use(deleteFile)
.use(listConverters) .use(listConverters)
.use(chooseConverter) .use(chooseConverter)
.onError(({ error }) => { .onError(({ error }) => {
console.error(error); console.error(error);
}); });
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
await import("./helpers/tailwind").then(async ({ generateTailwind }) => { await import("./helpers/tailwind").then(async ({ generateTailwind }) => {
const result = await generateTailwind(); const result = await generateTailwind();
app.get("/generated.css", ({ set }) => { app.get("/generated.css", ({ set }) => {
set.headers["content-type"] = "text/css"; set.headers["content-type"] = "text/css";
return result; return result;
}); });
}); });
} }
app.listen(3000); app.listen(3000);
console.log(`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`); console.log(`🦊 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(new Date(Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000).toISOString()); .all(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
rmSync(`${outputDir}${job.user_id}/${job.id}`, { rmSync(`${outputDir}${job.user_id}/${job.id}`, {
recursive: true, recursive: true,
force: true, force: true,
}); });
rmSync(`${uploadsDir}${job.user_id}/${job.id}`, { rmSync(`${uploadsDir}${job.user_id}/${job.id}`, {
recursive: true, recursive: true,
force: true, force: true,
}); });
// delete the job // delete the job
db.query("DELETE FROM jobs WHERE id = ?").run(job.id); db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
} }
setTimeout(clearJobs, AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000); setTimeout(clearJobs, AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000);
}; };
if (AUTO_DELETE_EVERY_N_HOURS > 0) { if (AUTO_DELETE_EVERY_N_HOURS > 0) {
clearJobs(); clearJobs();
} }

View File

@@ -1,32 +1,32 @@
@import "./theme/theme.css"; @import "./theme/theme.css";
@import "tailwindcss"; @import "tailwindcss";
@plugin "tailwind-scrollbar"; @plugin "tailwind-scrollbar";
@theme { @theme {
--color-contrast: var(--contrast); --color-contrast: var(--contrast);
--color-neutral-900: var(--neutral-900); --color-neutral-900: var(--neutral-900);
--color-neutral-800: var(--neutral-800); --color-neutral-800: var(--neutral-800);
--color-neutral-700: var(--neutral-700); --color-neutral-700: var(--neutral-700);
--color-neutral-600: var(--neutral-600); --color-neutral-600: var(--neutral-600);
--color-neutral-500: var(--neutral-500); --color-neutral-500: var(--neutral-500);
--color-neutral-400: var(--neutral-400); --color-neutral-400: var(--neutral-400);
--color-neutral-300: var(--neutral-300); --color-neutral-300: var(--neutral-300);
--color-neutral-200: var(--neutral-200); --color-neutral-200: var(--neutral-200);
--color-neutral-100: var(--neutral-100); --color-neutral-100: var(--neutral-100);
--color-accent-600: var(--accent-600); --color-accent-600: var(--accent-600);
--color-accent-500: var(--accent-500); --color-accent-500: var(--accent-500);
--color-accent-400: var(--accent-400); --color-accent-400: var(--accent-400);
} }
@utility article { @utility article {
@apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm; @apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm;
} }
@utility btn-primary { @utility btn-primary {
@apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors; @apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors;
} }
@utility btn-secondary { @utility btn-secondary {
@apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors; @apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors;
} }

View File

@@ -1,67 +1,67 @@
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import Elysia, { t } from "elysia"; import Elysia, { t } from "elysia";
import { getPossibleTargets } from "../converters/main"; import { getPossibleTargets } from "../converters/main";
import { userService } from "./user"; import { userService } from "./user";
export const chooseConverter = new Elysia().use(userService).post( export const chooseConverter = new Elysia().use(userService).post(
"/conversions", "/conversions",
({ body }) => { ({ body }) => {
return ( return (
<> <>
<article <article
class={` class={`
convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
overflow-x-hidden overflow-y-auto rounded bg-neutral-800 overflow-x-hidden overflow-y-auto rounded bg-neutral-800
sm:h-[30vh] sm:h-[30vh]
`} `}
> >
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => ( {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
<article <article
class={`convert_to_group flex w-full flex-col border-b border-neutral-700 p-4`} class={`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>
<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(([converter, targets]) => ( {Object.entries(getPossibleTargets(body.fileType)).map(([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

@@ -1,94 +1,94 @@
import { mkdir } from "node:fs/promises"; import { mkdir } from "node:fs/promises";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import { outputDir, uploadsDir } from ".."; import { outputDir, uploadsDir } from "..";
import { handleConvert } from "../converters/main"; import { handleConvert } from "../converters/main";
import db from "../db/db"; import db from "../db/db";
import { Jobs } from "../db/types"; import { Jobs } from "../db/types";
import { WEBROOT } from "../helpers/env"; import { WEBROOT } from "../helpers/env";
import { normalizeFiletype } from "../helpers/normalizeFiletype"; import { normalizeFiletype } from "../helpers/normalizeFiletype";
import { userService } from "./user"; import { userService } from "./user";
export const convert = new Elysia().use(userService).post( export const convert = new Elysia().use(userService).post(
"/convert", "/convert",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302); 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 = db const existingJob = db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?") .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.as(Jobs) .as(Jobs)
.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}/`;
const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`; const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`;
// create the output directory // create the output directory
try { try {
await mkdir(userOutputDir, { recursive: true }); await mkdir(userOutputDir, { recursive: true });
} catch (error) { } catch (error) {
console.error(`Failed to create the output directory: ${userOutputDir}.`, error); console.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] ?? "");
const converterName = body.convert_to.split(",")[1]; const converterName = body.convert_to.split(",")[1];
if (!converterName) { if (!converterName) {
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} }
const fileNames = JSON.parse(body.file_names) as string[]; const fileNames = JSON.parse(body.file_names) as string[];
for (let i = 0; i < fileNames.length; i++) { for (let i = 0; i < fileNames.length; i++) {
fileNames[i] = sanitize(fileNames[i] || ""); fileNames[i] = sanitize(fileNames[i] || "");
} }
if (!Array.isArray(fileNames) || fileNames.length === 0) { if (!Array.isArray(fileNames) || fileNames.length === 0) {
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} }
db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run( db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run(
fileNames.length, fileNames.length,
jobId.value, jobId.value,
); );
// Start the conversion process in the background // Start the conversion process in the background
handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId) handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId)
.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(jobId.value); db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
} }
// Delete all uploaded files in userUploadsDir // Delete all uploaded files in userUploadsDir
// rmSync(userUploadsDir, { recursive: true, force: true }); // rmSync(userUploadsDir, { recursive: true, force: true });
}) })
.catch((error) => { .catch((error) => {
console.error("Error in conversion process:", error); console.error("Error in conversion process:", error);
}); });
// Redirect the client immediately // Redirect the client immediately
return redirect(`${WEBROOT}/results/${jobId.value}`, 302); return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
}, },
{ {
body: t.Object({ body: t.Object({
convert_to: t.String(), convert_to: t.String(),
file_names: t.String(), file_names: t.String(),
}), }),
auth: true, auth: true,
}, },
); );

View File

@@ -1,32 +1,32 @@
import { unlink } from "node:fs/promises"; import { unlink } from "node:fs/promises";
import { Elysia, t } from "elysia"; 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 { WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
export const deleteFile = new Elysia().use(userService).post( export const deleteFile = new Elysia().use(userService).post(
"/delete", "/delete",
async ({ body, redirect, cookie: { jobId }, user }) => { async ({ body, redirect, cookie: { jobId }, user }) => {
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}`);
return { return {
message: "File deleted successfully.", message: "File deleted successfully.",
}; };
}, },
{ body: t.Object({ filename: t.String() }), auth: true }, { body: t.Object({ filename: t.String() }), auth: true },
); );

View File

@@ -1,61 +1,65 @@
import path from "node:path"; import path from "node:path";
import { Elysia, t } from 'elysia' import { Elysia } from "elysia";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import * as tar from "tar"; import * as tar from "tar";
import { outputDir } from ".."; import { outputDir } from "..";
import db from "../db/db"; import db from "../db/db";
import { WEBROOT } from "../helpers/env"; import { WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
export const download = new Elysia() export const download = new Elysia()
.use(userService) .use(userService)
.get( .get(
"/download/:userId/:jobId/:fileName", "/download/:userId/:jobId/:fileName",
async ({ params, redirect, user }) => { async ({ params, redirect, user }) => {
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);
} }
// parse from URL encoded string // parse from URL encoded string
const userId = decodeURIComponent(params.userId); const userId = decodeURIComponent(params.userId);
const jobId = decodeURIComponent(params.jobId); const jobId = decodeURIComponent(params.jobId);
const fileName = sanitize(decodeURIComponent(params.fileName)); const fileName = sanitize(decodeURIComponent(params.fileName));
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`; const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
return Bun.file(filePath); return Bun.file(filePath);
}, },
{ {
auth: true, auth: true,
} },
) )
.get("/archive/:userId/:jobId", async ({ params, redirect, user }) => { .get(
const job = await db "/archive/:userId/:jobId",
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") async ({ params, redirect, user }) => {
.get(user.id, params.jobId); const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
if (!job) { .get(user.id, params.jobId);
return redirect(`${WEBROOT}/results`, 302);
} if (!job) {
return redirect(`${WEBROOT}/results`, 302);
const userId = decodeURIComponent(params.userId); }
const jobId = decodeURIComponent(params.jobId);
const outputPath = `${outputDir}${userId}/${jobId}`; const userId = decodeURIComponent(params.userId);
const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`); const jobId = decodeURIComponent(params.jobId);
const outputPath = `${outputDir}${userId}/${jobId}`;
await tar.create( const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
{
file: outputTar, await tar.create(
cwd: outputPath, {
filter: (path) => { file: outputTar,
return !path.match(".*\\.tar"); cwd: outputPath,
}, filter: (path) => {
}, return !path.match(".*\\.tar");
["."], },
); },
return Bun.file(outputTar); ["."],
}, { );
auth: true, return Bun.file(outputTar);
}); },
{
auth: true,
},
);

View File

@@ -1,213 +1,215 @@
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { Elysia } from "elysia"; 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 db from "../db/db"; import db from "../db/db";
import { Filename, Jobs } from "../db/types"; import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env"; import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
export const history = new Elysia() export const history = new Elysia().use(userService).get(
.use(userService) "/history",
.get("/history", async ({ jwt, redirect, user }) => { async ({ redirect, user }) => {
if (HIDE_HISTORY) { if (HIDE_HISTORY) {
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} }
if (!user) { if (!user) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
let userJobs = db.query("SELECT * FROM jobs WHERE user_id = ?").as(Jobs).all(user.id).reverse(); let userJobs = db.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.query("SELECT * FROM file_names WHERE job_id = ?").as(Filename).all(job.id); const files = db.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;
} }
// Filter out jobs with no files // Filter out jobs with no files
userJobs = userJobs.filter((job) => job.num_files > 0); userJobs = userJobs.filter((job) => job.num_files > 0);
return ( return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Results"> <BaseHtml webroot={WEBROOT} title="ConvertX | Results">
<> <>
<Header <Header
webroot={WEBROOT} webroot={WEBROOT}
allowUnauthenticated={ALLOW_UNAUTHENTICATED} allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY} hideHistory={HIDE_HISTORY}
loggedIn loggedIn
/> />
<main <main
class={` class={`
w-full flex-1 px-2 w-full flex-1 px-2
sm:px-4 sm:px-4
`} `}
> >
<article class="article"> <article class="article">
<h1 class="mb-4 text-xl">Results</h1> <h1 class="mb-4 text-xl">Results</h1>
<table <table
class={` class={`
w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left w-full table-auto overflow-y-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> <tr>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
<span class="sr-only">Expand details</span> <span class="sr-only">Expand details</span>
</th> </th>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
Time Time
</th> </th>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
Files Files
</th> </th>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
max-sm:hidden max-sm:hidden
sm:px-4 sm:px-4
`} `}
> >
Files Done Files Done
</th> </th>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
Status Status
</th> </th>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
View View
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{userJobs.map((job) => ( {userJobs.map((job) => (
<> <>
<tr id={`job-row-${job.id}`}> <tr id={`job-row-${job.id}`}>
<td class="job-details-toggle cursor-pointer" data-job-id={job.id}> <td 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"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class="inline-block h-4 w-4" class="inline-block h-4 w-4"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5" d="M8.25 4.5l7.5 7.5-7.5 7.5"
/> />
</svg> </svg>
</td> </td>
<td safe>{new Date(job.date_created).toLocaleTimeString(LANGUAGE)}</td> <td safe>{new Date(job.date_created).toLocaleTimeString(LANGUAGE)}</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>
<td> <td>
<a <a
class={` class={`
text-accent-500 underline text-accent-500 underline
hover:text-accent-400 hover:text-accent-400
`} `}
href={`${WEBROOT}/results/${job.id}`} href={`${WEBROOT}/results/${job.id}`}
> >
View View
</a> </a>
</td> </td>
</tr> </tr>
<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">Detailed File Information:</div> <div class="mb-1 font-semibold">Detailed File Information:</div>
{job.files_detailed.map((file: Filename) => ( {job.files_detailed.map((file: Filename) => (
<div class="flex items-center"> <div class="flex items-center">
<span class="w-5/12 truncate" title={file.file_name} safe> <span class="w-5/12 truncate" title={file.file_name} safe>
{file.file_name} {file.file_name}
</span> </span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
class={`mx-2 inline-block h-4 w-4 text-neutral-500`} class={`mx-2 inline-block h-4 w-4 text-neutral-500`}
> >
<path <path
fill-rule="evenodd" 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" 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" clip-rule="evenodd"
/> />
</svg> </svg>
<span class="w-5/12 truncate" title={file.output_file_name} safe> <span class="w-5/12 truncate" title={file.output_file_name} safe>
{file.output_file_name} {file.output_file_name}
</span> </span>
</div> </div>
))} ))}
</div> </div>
</td> </td>
</tr> </tr>
</> </>
))} ))}
</tbody> </tbody>
</table> </table>
</article> </article>
</main> </main>
<script> <script>
{` {`
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const toggles = document.querySelectorAll('.job-details-toggle'); const toggles = document.querySelectorAll('.job-details-toggle');
toggles.forEach(toggle => { toggles.forEach(toggle => {
toggle.addEventListener('click', function() { toggle.addEventListener('click', function() {
const jobId = this.dataset.jobId; const jobId = this.dataset.jobId;
const detailsRow = document.getElementById(\`details-\${jobId}\`); const detailsRow = document.getElementById(\`details-\${jobId}\`);
// The arrow SVG itself has the ID arrow-\${jobId} // The arrow SVG itself has the ID arrow-\${jobId}
const arrow = document.getElementById(\`arrow-\${jobId}\`); const arrow = document.getElementById(\`arrow-\${jobId}\`);
if (detailsRow && arrow) { if (detailsRow && arrow) {
detailsRow.classList.toggle("hidden"); detailsRow.classList.toggle("hidden");
if (detailsRow.classList.contains("hidden")) { if (detailsRow.classList.contains("hidden")) {
// Right-facing arrow (collapsed) // Right-facing arrow (collapsed)
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />'; arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />';
} else { } else {
// Down-facing arrow (expanded) // Down-facing arrow (expanded)
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />'; arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />';
} }
} }
}); });
}); });
}); });
`} `}
</script> </script>
</> </>
</BaseHtml> </BaseHtml>
); );
}, { },
auth: true {
}); auth: true,
},
);

View File

@@ -1,73 +1,75 @@
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import Elysia from "elysia"; 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 { getAllInputs, getAllTargets } from "../converters/main"; import { getAllInputs, getAllTargets } from "../converters/main";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
export const listConverters = new Elysia() export const listConverters = new Elysia().use(userService).get(
.use(userService) "/converters",
.get("/converters", async () => { async () => {
return ( return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters"> <BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
<> <>
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn /> <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
<main <main
class={` class={`
w-full flex-1 px-2 w-full flex-1 px-2
sm:px-4 sm:px-4
`} `}
> >
<article class="article"> <article class="article">
<h1 class="mb-4 text-xl">Converters</h1> <h1 class="mb-4 text-xl">Converters</h1>
<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
[&_ul]:list-inside [&_ul]:list-disc [&_ul]:list-inside [&_ul]:list-disc
`} `}
> >
<thead> <thead>
<tr> <tr>
<th class="mx-4 my-2">Converter</th> <th class="mx-4 my-2">Converter</th>
<th class="mx-4 my-2">From (Count)</th> <th class="mx-4 my-2">From (Count)</th>
<th class="mx-4 my-2">To (Count)</th> <th class="mx-4 my-2">To (Count)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Object.entries(getAllTargets()).map(([converter, targets]) => { {Object.entries(getAllTargets()).map(([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>
</main> </main>
</> </>
</BaseHtml> </BaseHtml>
); );
}, { },
auth: true {
}); auth: true,
},
);

View File

@@ -1,207 +1,215 @@
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt"; import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia } from "elysia"; 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 db from "../db/db"; import db from "../db/db";
import { Filename, Jobs } from "../db/types"; import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
function ResultsArticle({ function ResultsArticle({
user, user,
job, job,
files, files,
outputPath, outputPath,
}: { }: {
user: { user: {
id: string; id: string;
} & JWTPayloadSpec; } & JWTPayloadSpec;
job: Jobs; job: Jobs;
files: Filename[]; files: Filename[];
outputPath: string; outputPath: string;
}) { }) {
return ( return (
<article class="article"> <article class="article">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-xl">Results</h1> <h1 class="text-xl">Results</h1>
<div> <div>
<a <a
style={files.length !== job.num_files ? "pointer-events: none;" : ""} style={files.length !== job.num_files ? "pointer-events: none;" : ""}
href={`${WEBROOT}/archive/${user.id}/${job.id}`} href={`${WEBROOT}/archive/${user.id}/${job.id}`}
download={`converted_files_${job.id}.tar`} download={`converted_files_${job.id}.tar`}
> >
<button <button
type="button" type="button"
class="float-right w-40 btn-primary" class="float-right w-40 btn-primary"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")} {...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
> >
{files.length === job.num_files ? "Download All" : "Converting..."} {files.length === job.num_files ? "Download All" : "Converting..."}
</button> </button>
</a> </a>
</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> <tr>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
Converted File Name Converted File Name
</th> </th>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
Status Status
</th> </th>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
View View
</th> </th>
<th <th
class={` class={`
px-2 py-2 px-2 py-2
sm:px-4 sm:px-4
`} `}
> >
Download Download
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{files.map((file) => ( {files.map((file) => (
<tr> <tr>
<td safe class="max-w-[20vw] truncate"> <td safe class="max-w-[20vw] truncate">
{file.output_file_name} {file.output_file_name}
</td> </td>
<td safe>{file.status}</td> <td safe>{file.status}</td>
<td> <td>
<a <a
class={` class={`
text-accent-500 underline text-accent-500 underline
hover:text-accent-400 hover:text-accent-400
`} `}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
> >
View View
</a> </a>
</td> </td>
<td> <td>
<a <a
class={` class={`
text-accent-500 underline text-accent-500 underline
hover:text-accent-400 hover:text-accent-400
`} `}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name} download={file.output_file_name}
> >
Download Download
</a> </a>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</article> </article>
); );
} }
export const results = new Elysia() export const results = new Elysia()
.use(userService) .use(userService)
.get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { job_id }, user }) => { .get(
if (job_id?.value) { "/results/:jobId",
// Clear the job_id cookie since we are viewing the results async ({ params, set, cookie: { job_id }, user }) => {
job_id.remove(); if (job_id?.value) {
} // Clear the job_id cookie since we are viewing the results
job_id.remove();
const job = db }
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) const job = db
.get(user.id, params.jobId); .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
if (!job) { .get(user.id, params.jobId);
set.status = 404;
return { if (!job) {
message: "Job not found.", set.status = 404;
}; return {
} message: "Job not found.",
};
const outputPath = `${user.id}/${params.jobId}/`; }
const files = db const outputPath = `${user.id}/${params.jobId}/`;
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename) const files = db
.all(params.jobId); .query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
return ( .all(params.jobId);
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
<> return (
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn /> <BaseHtml webroot={WEBROOT} title="ConvertX | Result">
<main <>
class={` <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
w-full flex-1 px-2 <main
sm:px-4 class={`
`} w-full flex-1 px-2
> sm:px-4
<ResultsArticle user={user} job={job} files={files} outputPath={outputPath} /> `}
</main> >
<script src={`${WEBROOT}/results.js`} defer /> <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />
</> </main>
</BaseHtml> <script src={`${WEBROOT}/results.js`} defer />
); </>
}, { auth: true }) </BaseHtml>
.post("/progress/:jobId", async ({ jwt, set, params, cookie: { job_id }, user }) => { );
if (job_id?.value) { },
// Clear the job_id cookie since we are viewing the results { auth: true },
job_id.remove(); )
} .post(
"/progress/:jobId",
const job = db async ({ set, params, cookie: { job_id }, user }) => {
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") if (job_id?.value) {
.as(Jobs) // Clear the job_id cookie since we are viewing the results
.get(user.id, params.jobId); job_id.remove();
}
if (!job) {
set.status = 404; const job = db
return { .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
message: "Job not found.", .as(Jobs)
}; .get(user.id, params.jobId);
}
if (!job) {
const outputPath = `${user.id}/${params.jobId}/`; set.status = 404;
return {
const files = db message: "Job not found.",
.query("SELECT * FROM file_names WHERE job_id = ?") };
.as(Filename) }
.all(params.jobId);
const outputPath = `${user.id}/${params.jobId}/`;
return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
}, { auth: true }); const files = db
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
.all(params.jobId);
return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
},
{ auth: true },
);

View File

@@ -1,248 +1,250 @@
import { randomInt } from "node:crypto"; import { randomInt } from "node:crypto";
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt"; import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia, t } from "elysia"; 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 { 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 { import {
ACCOUNT_REGISTRATION, ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED, ALLOW_UNAUTHENTICATED,
HIDE_HISTORY, HIDE_HISTORY,
HTTP_ALLOWED, HTTP_ALLOWED,
UNAUTHENTICATED_USER_SHARING, UNAUTHENTICATED_USER_SHARING,
WEBROOT, WEBROOT,
} from "../helpers/env"; } from "../helpers/env";
import { FIRST_RUN, userService } from "./user"; import { FIRST_RUN, userService } from "./user";
export const root = new Elysia() export const root = new Elysia().use(userService).get(
.use(userService) "/",
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { async ({ jwt, redirect, cookie: { auth, jobId } }) => {
if (!ALLOW_UNAUTHENTICATED) { if (!ALLOW_UNAUTHENTICATED) {
if (FIRST_RUN) { if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302); return redirect(`${WEBROOT}/setup`, 302);
} }
if (!auth?.value) { if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
} }
// validate jwt // validate jwt
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(
UNAUTHENTICATED_USER_SHARING UNAUTHENTICATED_USER_SHARING
? 0 ? 0
: randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), : randomInt(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,
}); });
user = { id: newUserId }; user = { id: newUserId };
if (!auth) { if (!auth) {
return { return {
message: "No auth cookie, perhaps your browser is blocking cookies.", message: "No auth cookie, perhaps your browser is blocking cookies.",
}; };
} }
// set cookie // set cookie
auth.set({ auth.set({
value: accessToken, value: accessToken,
httpOnly: true, httpOnly: true,
secure: !HTTP_ALLOWED, secure: !HTTP_ALLOWED,
maxAge: 24 * 60 * 60, maxAge: 24 * 60 * 60,
sameSite: "strict", sameSite: "strict",
}); });
} else if (auth?.value) { } else if (auth?.value) {
user = await jwt.verify(auth.value); user = await jwt.verify(auth.value);
if ( if (
user !== false && user !== false &&
user.id && user.id &&
(Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
) { ) {
// Make sure user exists in db // Make sure user exists in db
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).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);
} }
} }
} }
if (!user) { if (!user) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
// create a new job // create a new job
db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run( db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
user.id, user.id,
new Date().toISOString(), new Date().toISOString(),
); );
const { id } = db const { id } = 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 };
if (!jobId) { if (!jobId) {
return { message: "Cookies should be enabled to use this app." }; return { message: "Cookies should be enabled to use this app." };
} }
jobId.set({ jobId.set({
value: id, value: id,
httpOnly: true, httpOnly: true,
secure: !HTTP_ALLOWED, secure: !HTTP_ALLOWED,
maxAge: 24 * 60 * 60, maxAge: 24 * 60 * 60,
sameSite: "strict", sameSite: "strict",
}); });
console.log("jobId set to:", id); console.log("jobId set to:", id);
return ( return (
<BaseHtml webroot={WEBROOT}> <BaseHtml webroot={WEBROOT}>
<> <>
<Header <Header
webroot={WEBROOT} webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION} accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED} allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY} hideHistory={HIDE_HISTORY}
loggedIn loggedIn
/> />
<main <main
class={` class={`
w-full flex-1 px-2 w-full flex-1 px-2
sm:px-4 sm:px-4
`} `}
> >
<article class="article"> <article class="article">
<h1 class="mb-4 text-xl">Convert</h1> <h1 class="mb-4 text-xl">Convert</h1>
<div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto"> <div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto">
<table <table
id="file-list" id="file-list"
class={` class={`
w-full table-auto rounded bg-neutral-900 w-full table-auto rounded bg-neutral-900
[&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate [&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800 [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`} `}
/> />
</div> </div>
<div <div
id="dropzone" id="dropzone"
class={` class={`
relative flex h-48 w-full items-center justify-center rounded border border-dashed relative flex h-48 w-full items-center justify-center rounded border border-dashed
border-neutral-700 transition-all border-neutral-700 transition-all
hover:border-neutral-600 hover:border-neutral-600
[&.dragover]:border-4 [&.dragover]:border-neutral-500 [&.dragover]:border-4 [&.dragover]:border-neutral-500
`} `}
> >
<span> <span>
<b>Choose a file</b> or drag it here <b>Choose a file</b> or drag it here
</span> </span>
<input <input
type="file" type="file"
name="file" name="file"
multiple multiple
class="absolute inset-0 size-full cursor-pointer opacity-0" class="absolute inset-0 size-full cursor-pointer opacity-0"
/> />
</div> </div>
</article> </article>
<form <form
method="post" method="post"
action={`${WEBROOT}/convert`} action={`${WEBROOT}/convert`}
class="relative mx-auto mb-[35vh] w-full max-w-4xl" class="relative mx-auto mb-[35vh] w-full max-w-4xl"
> >
<input type="hidden" name="file_names" id="file_names" /> <input type="hidden" name="file_names" id="file_names" />
<article class="article w-full"> <article class="article w-full">
<input <input
type="search" type="search"
name="convert_to_search" name="convert_to_search"
placeholder="Search for conversions" placeholder="Search for conversions"
autocomplete="off" autocomplete="off"
class="w-full rounded-sm bg-neutral-800 p-4" class="w-full rounded-sm bg-neutral-800 p-4"
/> />
<div class="select_container relative"> <div class="select_container relative">
<article <article
class={` class={`
convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
overflow-x-hidden overflow-y-auto rounded bg-neutral-800 overflow-x-hidden overflow-y-auto rounded bg-neutral-800
sm:h-[30vh] sm:h-[30vh]
`} `}
> >
{Object.entries(getAllTargets()).map(([converter, targets]) => ( {Object.entries(getAllTargets()).map(([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 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(getAllTargets()).map(([converter, targets]) => ( {Object.entries(getAllTargets()).map(([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>
<input <input
class={` class={`
w-full btn-primary opacity-100 w-full btn-primary opacity-100
disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50
`} `}
type="submit" type="submit"
value="Convert" value="Convert"
disabled disabled
/> />
</form> </form>
</main> </main>
<script src="script.js" defer /> <script src="script.js" defer />
</> </>
</BaseHtml> </BaseHtml>
); );
}, { },
cookie: t.Cookie({ {
auth: t.Optional(t.String()), cookie: t.Cookie({
jobId: t.Optional(t.String()), auth: t.Optional(t.String()),
}) jobId: t.Optional(t.String()),
}); }),
},
);

View File

@@ -1,39 +1,39 @@
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import db from "../db/db"; import db from "../db/db";
import { WEBROOT } from "../helpers/env"; import { WEBROOT } from "../helpers/env";
import { uploadsDir } from "../index"; import { uploadsDir } from "../index";
import { userService } from "./user"; import { userService } from "./user";
export const upload = new Elysia().use(userService).post( export const upload = new Elysia().use(userService).post(
"/upload", "/upload",
async ({ body, redirect, user, cookie: { jobId } }) => { async ({ body, redirect, user, cookie: { jobId } }) => {
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}/`;
if (body?.file) { if (body?.file) {
if (Array.isArray(body.file)) { if (Array.isArray(body.file)) {
for (const file of body.file) { for (const file of body.file) {
await Bun.write(`${userUploadsDir}${file.name}`, file); await Bun.write(`${userUploadsDir}${file.name}`, file);
} }
} else { } else {
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file); 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() }), auth: true }, { body: t.Object({ file: t.Files() }), auth: true },
); );

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,47 @@
:root { :root {
/* Light mode */ /* Light mode */
--contrast: oklch(100% 0 0); --contrast: oklch(100% 0 0);
/* Neutral colors - Gray */ /* Neutral colors - Gray */
--neutral-950: oklch(98.5% 0.002 247.839); --neutral-950: oklch(98.5% 0.002 247.839);
--neutral-900: oklch(96.7% 0.003 264.542); --neutral-900: oklch(96.7% 0.003 264.542);
--neutral-800: oklch(92.8% 0.006 264.531); --neutral-800: oklch(92.8% 0.006 264.531);
--neutral-700: oklch(87.2% 0.01 258.338); --neutral-700: oklch(87.2% 0.01 258.338);
--neutral-600: oklch(70.7% 0.022 261.325); --neutral-600: oklch(70.7% 0.022 261.325);
--neutral-500: oklch(55.1% 0.027 264.364); --neutral-500: oklch(55.1% 0.027 264.364);
--neutral-400: oklch(44.6% 0.03 256.802); --neutral-400: oklch(44.6% 0.03 256.802);
--neutral-300: oklch(37.3% 0.034 259.733); --neutral-300: oklch(37.3% 0.034 259.733);
--neutral-200: oklch(26.9% 0 0); --neutral-200: oklch(26.9% 0 0);
--neutral-100: oklch(21% 0.034 264.665); --neutral-100: oklch(21% 0.034 264.665);
--neutral-50: oklch(13% 0.028 261.692); --neutral-50: oklch(13% 0.028 261.692);
/* lime-700 */ /* lime-700 */
--accent-600: oklch(53.2% 0.157 131.589); --accent-600: oklch(53.2% 0.157 131.589);
/* lime-600 */ /* lime-600 */
--accent-500: oklch(64.8% 0.2 131.684); --accent-500: oklch(64.8% 0.2 131.684);
/* lime-500 */ /* lime-500 */
--accent-400: oklch(76.8% 0.233 130.85); --accent-400: oklch(76.8% 0.233 130.85);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
/* Dark mode */ /* Dark mode */
:root { :root {
--contrast: oklch(0% 0 0); --contrast: oklch(0% 0 0);
/* Neutral colors - Gray */ /* Neutral colors - Gray */
--neutral-950: oklch(13% 0.028 261.692); --neutral-950: oklch(13% 0.028 261.692);
--neutral-900: oklch(21% 0.034 264.665); --neutral-900: oklch(21% 0.034 264.665);
--neutral-800: oklch(27.8% 0.033 256.848); --neutral-800: oklch(27.8% 0.033 256.848);
--neutral-700: oklch(37.3% 0.034 259.733); --neutral-700: oklch(37.3% 0.034 259.733);
--neutral-600: oklch(44.6% 0.03 256.802); --neutral-600: oklch(44.6% 0.03 256.802);
--neutral-500: oklch(55.1% 0.027 264.364); --neutral-500: oklch(55.1% 0.027 264.364);
--neutral-400: oklch(70.7% 0.022 261.325); --neutral-400: oklch(70.7% 0.022 261.325);
--neutral-300: oklch(87.2% 0.01 258.338); --neutral-300: oklch(87.2% 0.01 258.338);
--neutral-200: oklch(92.8% 0.006 264.531); --neutral-200: oklch(92.8% 0.006 264.531);
--neutral-100: oklch(96.7% 0.003 264.542); --neutral-100: oklch(96.7% 0.003 264.542);
--neutral-50: oklch(98.5% 0.002 247.839); --neutral-50: oklch(98.5% 0.002 247.839);
/* lime-600 */ /* lime-600 */
--accent-600: oklch(64.8% 0.2 131.684); --accent-600: oklch(64.8% 0.2 131.684);
/* lime-500 */ /* lime-500 */
--accent-500: oklch(76.8% 0.233 130.85); --accent-500: oklch(76.8% 0.233 130.85);
/* lime-400 */ /* lime-400 */
--accent-400: oklch(84.1% 0.238 128.85); --accent-400: oklch(84.1% 0.238 128.85);
} }
} }

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { convert } from "../../src/converters/assimp"; import { convert } from "../../src/converters/assimp";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert); runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {}); test.skip("dummy - required to trigger test detection", () => {});

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { convert } from "../../src/converters/calibre"; import { convert } from "../../src/converters/calibre";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert); runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {}); test.skip("dummy - required to trigger test detection", () => {});

View File

@@ -1,91 +1,91 @@
import type { ExecFileException } from "node:child_process"; import { beforeEach, expect, test } from "bun:test";
import { beforeEach, expect, test } from "bun:test"; import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/dvisvgm"; import { convert } from "../../src/converters/dvisvgm";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
let calls: string[][] = []; let calls: string[][] = [];
beforeEach(() => { beforeEach(() => {
calls = []; calls = [];
}); });
runCommonTests(convert); runCommonTests(convert);
test("convert respects eps filetype", async () => { test("convert respects eps filetype", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.eps", "eps", "stl", "output.stl", undefined, mockExecFile); const result = await convert("input.eps", "eps", "stl", "output.stl", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual(expect.arrayContaining(["--eps", "input.eps", "output.stl"])); expect(calls[0]).toEqual(expect.arrayContaining(["--eps", "input.eps", "output.stl"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("convert respects pdf filetype", async () => { test("convert respects pdf filetype", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.pdf", "pdf", "stl", "output.stl", undefined, mockExecFile); const result = await convert("input.pdf", "pdf", "stl", "output.stl", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual(expect.arrayContaining(["--pdf", "input.pdf", "output.stl"])); expect(calls[0]).toEqual(expect.arrayContaining(["--pdf", "input.pdf", "output.stl"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("convert respects svgz conversion target type", async () => { test("convert respects svgz conversion target type", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.obj", "eps", "svgz", "output.svgz", undefined, mockExecFile); const result = await convert("input.obj", "eps", "svgz", "output.svgz", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual(expect.arrayContaining(["-z", "input.obj", "output.svgz"])); expect(calls[0]).toEqual(expect.arrayContaining(["-z", "input.obj", "output.svgz"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });

View File

@@ -1,181 +1,181 @@
import { beforeEach, expect, test } from "bun:test"; import { beforeEach, expect, test } from "bun:test";
import { convert } from "../../src/converters/ffmpeg"; import { convert } from "../../src/converters/ffmpeg";
let calls: string[][] = []; let calls: string[][] = [];
function mockExecFile( function mockExecFile(
_cmd: string, _cmd: string,
args: string[], args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void, callback: (err: Error | null, stdout: string, stderr: string) => void,
) { ) {
calls.push(args); calls.push(args);
if (args.includes("fail.mov")) { if (args.includes("fail.mov")) {
callback(new Error("mock failure"), "", "Fake stderr: fail"); callback(new Error("mock failure"), "", "Fake stderr: fail");
} else { } else {
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
} }
} }
beforeEach(() => { beforeEach(() => {
calls = []; calls = [];
delete process.env.FFMPEG_ARGS; delete process.env.FFMPEG_ARGS;
}); });
test("converts a normal file", async () => { test("converts a normal file", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const result = await convert("in.mp4", "mp4", "avi", "out.avi", undefined, mockExecFile); const result = await convert("in.mp4", "mp4", "avi", "out.avi", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual(expect.arrayContaining(["-i", "in.mp4", "out.avi"])); expect(calls[0]).toEqual(expect.arrayContaining(["-i", "in.mp4", "out.avi"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("adds resize for ico output", async () => { test("adds resize for ico output", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const result = await convert("in.png", "png", "ico", "out.ico", undefined, mockExecFile); const result = await convert("in.png", "png", "ico", "out.ico", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done: resized to 256x256"); expect(result).toBe("Done: resized to 256x256");
expect(calls[0]).toEqual( expect(calls[0]).toEqual(
expect.arrayContaining(["-filter:v", expect.stringContaining("scale=")]), expect.arrayContaining(["-filter:v", expect.stringContaining("scale=")]),
); );
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("uses libaom-av1 for av1.mp4", async () => { test("uses libaom-av1 for av1.mp4", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
await convert("in.mkv", "mkv", "av1.mp4", "out.mp4", undefined, mockExecFile); await convert("in.mkv", "mkv", "av1.mp4", "out.mp4", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libaom-av1"])); expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libaom-av1"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("uses libx264 for h264.mp4", async () => { test("uses libx264 for h264.mp4", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
await convert("in.mkv", "mkv", "h264.mp4", "out.mp4", undefined, mockExecFile); await convert("in.mkv", "mkv", "h264.mp4", "out.mp4", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx264"])); expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx264"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("uses libx265 for h265.mp4", async () => { test("uses libx265 for h265.mp4", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
await convert("in.mkv", "mkv", "h265.mp4", "out.mp4", undefined, mockExecFile); await convert("in.mkv", "mkv", "h265.mp4", "out.mp4", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx265"])); expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx265"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("uses libx266 for h266.mp4", async () => { test("uses libx266 for h266.mp4", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
await convert("in.mkv", "mkv", "h266.mp4", "out.mp4", undefined, mockExecFile); await convert("in.mkv", "mkv", "h266.mp4", "out.mp4", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx266"])); expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx266"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("respects FFMPEG_ARGS", async () => { test("respects FFMPEG_ARGS", async () => {
process.env.FFMPEG_ARGS = "-hide_banner -y"; process.env.FFMPEG_ARGS = "-hide_banner -y";
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile); await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(calls[0]?.slice(0, 2)).toEqual(["-hide_banner", "-y"]); expect(calls[0]?.slice(0, 2)).toEqual(["-hide_banner", "-y"]);
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("fails on exec error", async () => { test("fails on exec error", async () => {
const originalConsoleError = console.error; const originalConsoleError = console.error;
let loggedMessage = ""; let loggedMessage = "";
console.error = (msg) => { console.error = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
expect(convert("fail.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile)).rejects.toThrow( expect(convert("fail.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile)).rejects.toThrow(
"mock failure", "mock failure",
); );
console.error = originalConsoleError; console.error = originalConsoleError;
expect(loggedMessage).toBe("stderr: Fake stderr: fail"); expect(loggedMessage).toBe("stderr: Fake stderr: fail");
}); });
test("logs stderr when execFile returns only stderr and no error", async () => { test("logs stderr when execFile returns only stderr and no error", async () => {
const originalConsoleError = console.error; const originalConsoleError = console.error;
let loggedMessage = ""; let loggedMessage = "";
console.error = (msg) => { console.error = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
// Mock execFile to call back with no error, no stdout, but with stderr // Mock execFile to call back with no error, no stdout, but with stderr
const mockExecFileStderrOnly = ( const mockExecFileStderrOnly = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void, callback: (err: Error | null, stdout: string, stderr: string) => void,
) => { ) => {
callback(null, "", "Only stderr output"); callback(null, "", "Only stderr output");
}; };
await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFileStderrOnly); await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFileStderrOnly);
console.error = originalConsoleError; console.error = originalConsoleError;
expect(loggedMessage).toBe("stderr: Only stderr output"); expect(loggedMessage).toBe("stderr: Only stderr output");
}); });

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { convert } from "../../src/converters/graphicsmagick"; import { convert } from "../../src/converters/graphicsmagick";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert); runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {}); test.skip("dummy - required to trigger test detection", () => {});

View File

@@ -1,26 +1,26 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { ConvertFnWithExecFile } from "../../../src/converters/types"; import { ConvertFnWithExecFile } from "../../../src/converters/types";
import { import {
runConvertFailTest, runConvertFailTest,
runConvertLogsStderror, runConvertLogsStderror,
runConvertLogsStderrorAndStdout, runConvertLogsStderrorAndStdout,
runConvertSuccessTest, runConvertSuccessTest,
} from "./converters"; } from "./converters";
export function runCommonTests(convert: ConvertFnWithExecFile) { export function runCommonTests(convert: ConvertFnWithExecFile) {
test("convert resolves when execFile succeeds", async () => { test("convert resolves when execFile succeeds", async () => {
await runConvertSuccessTest(convert); await runConvertSuccessTest(convert);
}); });
test("convert rejects when execFile fails", async () => { test("convert rejects when execFile fails", async () => {
await runConvertFailTest(convert); await runConvertFailTest(convert);
}); });
test("convert logs stderr when present", async () => { test("convert logs stderr when present", async () => {
await runConvertLogsStderror(convert); await runConvertLogsStderror(convert);
}); });
test("convert logs both stderr and stdout when present", async () => { test("convert logs both stderr and stdout when present", async () => {
await runConvertLogsStderrorAndStdout(convert); await runConvertLogsStderrorAndStdout(convert);
}); });
} }

View File

@@ -1,121 +1,121 @@
import type { ExecFileException } from "node:child_process"; import { expect } from "bun:test";
import { expect } from "bun:test"; import type { ExecFileException } from "node:child_process";
import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types"; import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types";
export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) { export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile); const result = await convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
} }
export async function runConvertFailTest(convertFn: ConvertFnWithExecFile) { export async function runConvertFailTest(convertFn: ConvertFnWithExecFile) {
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
callback(new Error("Test error"), "", ""); callback(new Error("Test error"), "", "");
}; };
expect( expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile), convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile),
).rejects.toMatch(/error: Error: Test error/); ).rejects.toMatch(/error: Error: Test error/);
// Test with error object lacking 'message' property // Test with error object lacking 'message' property
const mockExecFileNoMessage: ExecFileFn = ( const mockExecFileNoMessage: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
// Simulate a non-standard error object // Simulate a non-standard error object
callback({ notMessage: true } as unknown as ExecFileException, "", ""); callback({ notMessage: true } as unknown as ExecFileException, "", "");
}; };
expect( expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileNoMessage), convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileNoMessage),
).rejects.toMatch(/error:/i); ).rejects.toMatch(/error:/i);
// Test with a non-object error (e.g., a string) // Test with a non-object error (e.g., a string)
const mockExecFileStringError: ExecFileFn = ( const mockExecFileStringError: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
callback("string error" as unknown as ExecFileException, "", ""); callback("string error" as unknown as ExecFileException, "", "");
}; };
expect( expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileStringError), convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileStringError),
).rejects.toMatch(/error:/i); ).rejects.toMatch(/error:/i);
} }
export async function runConvertLogsStderror(convertFn: ConvertFnWithExecFile) { export async function runConvertLogsStderror(convertFn: ConvertFnWithExecFile) {
const originalConsoleError = console.error; const originalConsoleError = console.error;
let loggedMessage = ""; let loggedMessage = "";
console.error = (msg) => { console.error = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile = ( const mockExecFile = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void, callback: (err: Error | null, stdout: string, stderr: string) => void,
) => { ) => {
callback(null, "", "Fake stderr"); callback(null, "", "Fake stderr");
}; };
await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile); await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
console.error = originalConsoleError; console.error = originalConsoleError;
expect(loggedMessage).toBe("stderr: Fake stderr"); expect(loggedMessage).toBe("stderr: Fake stderr");
} }
export async function runConvertLogsStderrorAndStdout(convertFn: ConvertFnWithExecFile) { export async function runConvertLogsStderrorAndStdout(convertFn: ConvertFnWithExecFile) {
const originalConsoleError = console.error; const originalConsoleError = console.error;
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedError = ""; let loggedError = "";
let loggedMessage = ""; let loggedMessage = "";
console.error = (msg) => { console.error = (msg) => {
loggedError = msg; loggedError = msg;
}; };
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile = ( const mockExecFile = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void, callback: (err: Error | null, stdout: string, stderr: string) => void,
) => { ) => {
callback(null, "Fake stdout", "Fake stderr"); callback(null, "Fake stdout", "Fake stderr");
}; };
await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile); await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
console.error = originalConsoleError; console.error = originalConsoleError;
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(loggedError).toBe("stderr: Fake stderr"); expect(loggedError).toBe("stderr: Fake stderr");
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
} }

View File

@@ -1,165 +1,165 @@
import type { ExecFileException } from "node:child_process"; import { beforeEach, expect, test } from "bun:test";
import { beforeEach, expect, test } from "bun:test"; import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/imagemagick"; import { convert } from "../../src/converters/imagemagick";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
let calls: string[][] = []; let calls: string[][] = [];
beforeEach(() => { beforeEach(() => {
calls = []; calls = [];
}); });
runCommonTests(convert); runCommonTests(convert);
test("convert respects ico conversion target type", async () => { test("convert respects ico conversion target type", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.obj", "eps", "ico", "output.ico", undefined, mockExecFile); const result = await convert("input.obj", "eps", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual( expect(calls[0]).toEqual(
expect.arrayContaining([ expect.arrayContaining([
"-define", "-define",
"icon:auto-resize=256,128,64,48,32,16", "icon:auto-resize=256,128,64,48,32,16",
"-background", "-background",
"none", "none",
"input.obj", "input.obj",
"output.ico", "output.ico",
]), ]),
); );
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("convert respects ico conversion target type with svg as input filetype", async () => { test("convert respects ico conversion target type with svg as input filetype", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.svg", "svg", "ico", "output.ico", undefined, mockExecFile); const result = await convert("input.svg", "svg", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual( expect(calls[0]).toEqual(
expect.arrayContaining([ expect.arrayContaining([
"-define", "-define",
"icon:auto-resize=256,128,64,48,32,16", "icon:auto-resize=256,128,64,48,32,16",
"-background", "-background",
"none", "none",
"-density", "-density",
"512", "512",
"input.svg", "input.svg",
"output.ico", "output.ico",
]), ]),
); );
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("convert respects ico conversion target type with emf as input filetype", async () => { test("convert respects ico conversion target type with emf as input filetype", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.emf", "emf", "ico", "output.ico", undefined, mockExecFile); const result = await convert("input.emf", "emf", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual( expect(calls[0]).toEqual(
expect.arrayContaining([ expect.arrayContaining([
"-define", "-define",
"icon:auto-resize=256,128,64,48,32,16", "icon:auto-resize=256,128,64,48,32,16",
"-background", "-background",
"none", "none",
"emf:delegate=false", "emf:delegate=false",
"-density", "-density",
"300", "300",
"white", "white",
"-alpha", "-alpha",
"remove", "remove",
"input.emf", "input.emf",
"output.ico", "output.ico",
]), ]),
); );
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("convert respects emf as input filetype", async () => { test("convert respects emf as input filetype", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.emf", "emf", "obj", "output.obj", undefined, mockExecFile); const result = await convert("input.emf", "emf", "obj", "output.obj", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual( expect(calls[0]).toEqual(
expect.arrayContaining([ expect.arrayContaining([
"-define", "-define",
"emf:delegate=false", "emf:delegate=false",
"-density", "-density",
"300", "300",
"-background", "-background",
"white", "white",
"-alpha", "-alpha",
"remove", "remove",
"input.emf", "input.emf",
"output.obj", "output.obj",
]), ]),
); );
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { convert } from "../../src/converters/inkscape"; import { convert } from "../../src/converters/inkscape";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert); runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {}); test.skip("dummy - required to trigger test detection", () => {});

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { convert } from "../../src/converters/libheif"; import { convert } from "../../src/converters/libheif";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert); runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {}); test.skip("dummy - required to trigger test detection", () => {});

View File

@@ -1,91 +1,91 @@
import type { ExecFileException } from "node:child_process"; import { beforeEach, expect, test } from "bun:test";
import { beforeEach, expect, test } from "bun:test"; import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/libjxl"; import { convert } from "../../src/converters/libjxl";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
let command: string = ""; let command: string = "";
beforeEach(() => { beforeEach(() => {
command = ""; command = "";
}); });
runCommonTests(convert); runCommonTests(convert);
test("convert uses djxl with input filetype being jxl", async () => { test("convert uses djxl with input filetype being jxl", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
command = _cmd; command = _cmd;
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.jxl", "jxl", "png", "output.png", undefined, mockExecFile); const result = await convert("input.jxl", "jxl", "png", "output.png", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(command).toEqual("djxl"); expect(command).toEqual("djxl");
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("convert uses cjxl with output filetype being jxl", async () => { test("convert uses cjxl with output filetype being jxl", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
command = _cmd; command = _cmd;
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.png", "png", "jxl", "output.jxl", undefined, mockExecFile); const result = await convert("input.png", "png", "jxl", "output.jxl", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(command).toEqual("cjxl"); expect(command).toEqual("cjxl");
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("convert uses empty string as command with neither input nor output filetype being jxl", async () => { test("convert uses empty string as command with neither input nor output filetype being jxl", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
command = _cmd; command = _cmd;
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.png", "png", "jpg", "output.jpg", undefined, mockExecFile); const result = await convert("input.png", "png", "jpg", "output.jpg", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(command).toEqual(""); expect(command).toEqual("");
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });

View File

@@ -1,61 +1,61 @@
import type { ExecFileException } from "node:child_process"; import { expect, test } from "bun:test";
import { expect, test } from "bun:test"; import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/msgconvert"; import { convert } from "../../src/converters/msgconvert";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
test("convert rejects conversion if input filetype is not msg and output type is not eml", async () => { test("convert rejects conversion if input filetype is not msg and output type is not eml", async () => {
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const expectedError = new Error( const expectedError = new Error(
"Unsupported conversion from obj to stl. Only MSG to EML conversion is currently supported.", "Unsupported conversion from obj to stl. Only MSG to EML conversion is currently supported.",
); );
expect(convert("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile)).rejects.toEqual( expect(convert("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile)).rejects.toEqual(
expectedError, expectedError,
); );
}); });
test("convert rejects conversion on error", async () => { test("convert rejects conversion on error", async () => {
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
callback(new Error("Test error"), "", ""); callback(new Error("Test error"), "", "");
}; };
const expectedError = new Error("msgconvert failed: Test error"); const expectedError = new Error("msgconvert failed: Test error");
expect(convert("input.msg", "msg", "eml", "output.eml", undefined, mockExecFile)).rejects.toEqual( expect(convert("input.msg", "msg", "eml", "output.eml", undefined, mockExecFile)).rejects.toEqual(
expectedError, expectedError,
); );
}); });
test("convert logs stderr as warning", async () => { test("convert logs stderr as warning", async () => {
const originalConsoleWarn = console.warn; const originalConsoleWarn = console.warn;
let loggedMessage = ""; let loggedMessage = "";
console.warn = (msg) => { console.warn = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile = ( const mockExecFile = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void, callback: (err: Error | null, stdout: string, stderr: string) => void,
) => { ) => {
callback(null, "", "Fake stderr"); callback(null, "", "Fake stderr");
}; };
await convert("file.msg", "msg", "eml", "out.eml", undefined, mockExecFile); await convert("file.msg", "msg", "eml", "out.eml", undefined, mockExecFile);
console.error = originalConsoleWarn; console.error = originalConsoleWarn;
expect(loggedMessage).toBe("msgconvert stderr: Fake stderr"); expect(loggedMessage).toBe("msgconvert stderr: Fake stderr");
}); });

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { convert } from "../../src/converters/potrace"; import { convert } from "../../src/converters/potrace";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert); runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {}); test.skip("dummy - required to trigger test detection", () => {});

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { convert } from "../../src/converters/resvg"; import { convert } from "../../src/converters/resvg";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert); runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {}); test.skip("dummy - required to trigger test detection", () => {});

View File

@@ -1,65 +1,65 @@
import type { ExecFileException } from "node:child_process"; import { beforeEach, expect, test } from "bun:test";
import { beforeEach, expect, test } from "bun:test"; import type { ExecFileException } from "node:child_process";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { convert } from "../../src/converters/vips"; import { convert } from "../../src/converters/vips";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
let calls: string[][] = []; let calls: string[][] = [];
beforeEach(() => { beforeEach(() => {
calls = []; calls = [];
}); });
runCommonTests(convert); runCommonTests(convert);
test("convert uses action pdfload with filetype being pdf", async () => { test("convert uses action pdfload with filetype being pdf", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.pdf", "pdf", "obj", "output.obj", undefined, mockExecFile); const result = await convert("input.pdf", "pdf", "obj", "output.obj", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual(expect.arrayContaining(["pdfload"])); expect(calls[0]).toEqual(expect.arrayContaining(["pdfload"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });
test("convert uses action copy with filetype being anything but pdf", async () => { test("convert uses action copy with filetype being anything but pdf", async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
let loggedMessage = ""; let loggedMessage = "";
console.log = (msg) => { console.log = (msg) => {
loggedMessage = msg; loggedMessage = msg;
}; };
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
_cmd: string, _cmd: string,
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => { ) => {
calls.push(_args); calls.push(_args);
callback(null, "Fake stdout", ""); callback(null, "Fake stdout", "");
}; };
const result = await convert("input.jpg", "jpg", "obj", "output.obj", undefined, mockExecFile); const result = await convert("input.jpg", "jpg", "obj", "output.obj", undefined, mockExecFile);
console.log = originalConsoleLog; console.log = originalConsoleLog;
expect(result).toBe("Done"); expect(result).toBe("Done");
expect(calls[0]).toEqual(expect.arrayContaining(["copy"])); expect(calls[0]).toEqual(expect.arrayContaining(["copy"]));
expect(loggedMessage).toBe("stdout: Fake stdout"); expect(loggedMessage).toBe("stdout: Fake stdout");
}); });

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test"; import { test } from "bun:test";
import { convert } from "../../src/converters/xelatex"; import { convert } from "../../src/converters/xelatex";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert); runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {}); test.skip("dummy - required to trigger test detection", () => {});

View File

@@ -1,34 +1,34 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ESNext"], "lib": ["ESNext"],
"module": "ESNext", "module": "ESNext",
"target": "ES2021", "target": "ES2021",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"moduleDetection": "force", "moduleDetection": "force",
// "allowImportingTsExtensions": true, // "allowImportingTsExtensions": true,
"outDir": "dist", "outDir": "dist",
"noEmit": false, "noEmit": false,
"composite": true, "composite": true,
"strict": true, "strict": true,
"downlevelIteration": true, "downlevelIteration": true,
"skipLibCheck": true, "skipLibCheck": true,
"jsx": "react", "jsx": "react",
"jsxFactory": "Html.createElement", "jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment", "jsxFragmentFactory": "Html.Fragment",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"allowJs": true, "allowJs": true,
// non bun init // non bun init
"plugins": [{ "name": "@kitajs/ts-html-plugin" }], "plugins": [{ "name": "@kitajs/ts-html-plugin" }],
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
// "noUnusedLocals": true, // "noUnusedLocals": true,
// "noUnusedParameters": true, // "noUnusedParameters": true,
"exactOptionalPropertyTypes": true, "exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true "esModuleInterop": true
// "noImplicitReturns": true // "noImplicitReturns": true
}, },
"include": ["src", "tests", "package.json", "reset.d.ts"] "include": ["src", "tests", "package.json", "reset.d.ts"]
} }