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

View File

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

30
.github/FUNDING.yml vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,27 @@
name: Update Docker Hub Description
env:
IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: c4illin
on:
push:
branches:
- main
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.IMAGE_NAME }}
short-description: ${{ github.event.repository.description }}
enable-url-completion: true
name: Update Docker Hub Description
env:
IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: c4illin
on:
push:
branches:
- main
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.IMAGE_NAME }}
short-description: ${{ github.event.repository.description }}
enable-url-completion: true

View File

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

View File

@@ -1,21 +1,21 @@
name: Remove Docker Tag
on:
workflow_dispatch:
jobs:
remove-docker-tag:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
# (required)
permissions:
contents: read
packages: write
steps:
- name: Remove Docker Tag
uses: ArchieAtkinson/remove-dockertag-action@v0.0
with:
tag_name: master
github_token: ${{ secrets.GITHUB_TOKEN }}
name: Remove Docker Tag
on:
workflow_dispatch:
jobs:
remove-docker-tag:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
# (required)
permissions:
contents: read
packages: write
steps:
- name: Remove Docker Tag
uses: ArchieAtkinson/remove-dockertag-action@v0.0
with:
tag_name: master
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

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

View File

@@ -1,233 +1,233 @@
# Changelog
## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04)
### Bug Fixes
- 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)
### Features
- 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)
- 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))
- show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26))
### 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)
- progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7))
- 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)
## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14)
### Features
- 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 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))
### Bug Fixes
- 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)
### Bug Fixes
- 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)
### Features
- 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))
- replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3))
### Bug Fixes
- 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)
- 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))
- 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)
### Bug Fixes
- 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)
### Features
- add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192)
### Bug Fixes
- 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))
## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21)
### Bug Fixes
- 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)
### Features
- add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191)
### 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 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))
## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21)
### Features
- 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)
- disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
### Bug Fixes
- 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)
## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05)
### 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)
- 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)
## [0.8.0](https://github.com/C4illin/ConvertX/compare/v0.7.0...v0.8.0) (2024-09-30)
### Features
- add light theme, fixes [#156](https://github.com/C4illin/ConvertX/issues/156) ([72636c5](https://github.com/C4illin/ConvertX/commit/72636c5059ebf09c8fece2e268293650b2f8ccf6))
### Bug Fixes
- 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))
- 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)
### Features
- Add support for 3d assets through assimp converter ([63a4328](https://github.com/C4illin/ConvertX/commit/63a4328d4a1e01df3e0ec4a877bad8c8ffe71129))
### Bug Fixes
- 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)
### Features
- ui remake with tailwind ([22f823c](https://github.com/C4illin/ConvertX/commit/22f823c535b20382981f86a13616b830a1f3392f))
### 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))
## [0.5.0](https://github.com/C4illin/ConvertX/compare/v0.4.1...v0.5.0) (2024-09-20)
### Features
- add option to customize how often files are automatically deleted ([317c932](https://github.com/C4illin/ConvertX/commit/317c932c2a26280bf37ed3d3bf9b879413590f5a))
### Bug Fixes
- 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)
### 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))
## [0.4.0](https://github.com/C4illin/ConvertX/compare/v0.3.3...v0.4.0) (2024-08-26)
### Features
- 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 robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7))
- Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58))
### 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))
- 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))
## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30)
### Bug Fixes
- downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c))
## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09)
### Bug Fixes
- increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27)
### Bug Fixes
- release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27)
### Features
- add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
- change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
- print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20)
### Features
- add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
- change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10)
### Bug Fixes
- fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30)
### Bug Fixes
- :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
## 0.1.0 (2024-05-30)
### Features
- remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
### Miscellaneous Chores
- release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431))
# Changelog
## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04)
### Bug Fixes
- 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)
### Features
- 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)
- 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))
- show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26))
### 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)
- progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7))
- 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)
## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14)
### Features
- 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 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))
### Bug Fixes
- 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)
### Bug Fixes
- 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)
### Features
- 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))
- replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3))
### Bug Fixes
- 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)
- 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))
- 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)
### Bug Fixes
- 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)
### Features
- add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192)
### Bug Fixes
- 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))
## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21)
### Bug Fixes
- 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)
### Features
- add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191)
### 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 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))
## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21)
### Features
- 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)
- disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
### Bug Fixes
- 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)
## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05)
### 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)
- 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)
## [0.8.0](https://github.com/C4illin/ConvertX/compare/v0.7.0...v0.8.0) (2024-09-30)
### Features
- add light theme, fixes [#156](https://github.com/C4illin/ConvertX/issues/156) ([72636c5](https://github.com/C4illin/ConvertX/commit/72636c5059ebf09c8fece2e268293650b2f8ccf6))
### Bug Fixes
- 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))
- 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)
### Features
- Add support for 3d assets through assimp converter ([63a4328](https://github.com/C4illin/ConvertX/commit/63a4328d4a1e01df3e0ec4a877bad8c8ffe71129))
### Bug Fixes
- 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)
### Features
- ui remake with tailwind ([22f823c](https://github.com/C4illin/ConvertX/commit/22f823c535b20382981f86a13616b830a1f3392f))
### 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))
## [0.5.0](https://github.com/C4illin/ConvertX/compare/v0.4.1...v0.5.0) (2024-09-20)
### Features
- add option to customize how often files are automatically deleted ([317c932](https://github.com/C4illin/ConvertX/commit/317c932c2a26280bf37ed3d3bf9b879413590f5a))
### Bug Fixes
- 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)
### 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))
## [0.4.0](https://github.com/C4illin/ConvertX/compare/v0.3.3...v0.4.0) (2024-08-26)
### Features
- 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 robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7))
- Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58))
### 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))
- 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))
## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30)
### Bug Fixes
- downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c))
## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09)
### Bug Fixes
- increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27)
### Bug Fixes
- release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27)
### Features
- add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
- change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
- print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20)
### Features
- add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
- change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10)
### Bug Fixes
- fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30)
### Bug Fixes
- :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
## 0.1.0 (2024-05-30)
### Features
- remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
### Miscellaneous Chores
- 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
[![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)
[![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 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)
![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>
<!-- ![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.
## Features
- Convert files to different formats
- Process multiple files at once
- Password protection
- Multiple accounts
## Converters supported
| Converter | Use case | Converts from | Converts to |
| -------------------------------------------------- | ---------------- | ------------- | ----------- |
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [dvisvgm](https://dvisvgm.de/) | Vector images | 4 | 2 |
| [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 |
| [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 |
| [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 |
| [VTracer](https://github.com/visioncortex/vtracer) | Raster to vector | 8 | 1 |
| [Dasel](https://github.com/TomWright/dasel) | Data Files | 5 | 4 |
<!-- many ffmpeg fileformats are duplicates -->
Any missing converter? Open an issue or pull request!
## Deployment
> [!WARNING]
> If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true
```yml
# docker-compose.yml
services:
convertx:
image: ghcr.io/c4illin/convertx
container_name: convertx
restart: unless-stopped
ports:
- "3000:3000"
environment:
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
# - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection
volumes:
- ./data:/app/data
```
or
```bash
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.
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
### Environment variables
All are optional, JWT_SECRET is recommended to be set.
| 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 |
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
| 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 |
| 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/" |
| FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` |
| 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) |
| UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users |
### 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.
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: ghcr.io/c4illin/convertx` | The latest release 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: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=)
![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 -->
### Tutorial
> [!NOTE]
> These are written by other people, and may be outdated, incorrect or wrong.
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
Tutorial in chinese: <https://xzllll.com/24092901/>
## Screenshots
![ConvertX Preview](images/preview.png)
## Development
0. Install [Bun](https://bun.sh/) and Git
1. Clone the repository
2. `bun install`
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!
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
## Contributors
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
</a>
## Star History
<a href="https://github.com/C4illin/ConvertX/stargazers">
<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: 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" />
</picture>
</a>
![ConvertX](images/logo.png)
# 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)
[![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)
[![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 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=)
<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=) -->
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
## Features
- Convert files to different formats
- Process multiple files at once
- Password protection
- Multiple accounts
## Converters supported
| Converter | Use case | Converts from | Converts to |
| -------------------------------------------------- | ---------------- | ------------- | ----------- |
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [dvisvgm](https://dvisvgm.de/) | Vector images | 4 | 2 |
| [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 |
| [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 |
| [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 |
| [VTracer](https://github.com/visioncortex/vtracer) | Raster to vector | 8 | 1 |
| [Dasel](https://github.com/TomWright/dasel) | Data Files | 5 | 4 |
<!-- many ffmpeg fileformats are duplicates -->
Any missing converter? Open an issue or pull request!
## Deployment
> [!WARNING]
> If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true
```yml
# docker-compose.yml
services:
convertx:
image: ghcr.io/c4illin/convertx
container_name: convertx
restart: unless-stopped
ports:
- "3000:3000"
environment:
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
# - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection
volumes:
- ./data:/app/data
```
or
```bash
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.
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
### Environment variables
All are optional, JWT_SECRET is recommended to be set.
| 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 |
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
| 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 |
| 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/" |
| FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` |
| 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) |
| UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users |
### 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.
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: ghcr.io/c4illin/convertx` | The latest release 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: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=)
![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 -->
### Tutorial
> [!NOTE]
> These are written by other people, and may be outdated, incorrect or wrong.
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
Tutorial in chinese: <https://xzllll.com/24092901/>
## Screenshots
![ConvertX Preview](images/preview.png)
## Development
0. Install [Bun](https://bun.sh/) and Git
1. Clone the repository
2. `bun install`
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!
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
## Contributors
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
</a>
## Star History
<a href="https://github.com/C4illin/ConvertX/stargazers">
<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: 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" />
</picture>
</a>

View File

@@ -1,9 +1,9 @@
# Security Policy
## Supported Versions
Only the latest release is supported
## 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.
# Security Policy
## Supported Versions
Only the latest release is supported
## 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.

View File

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

View File

@@ -21,7 +21,6 @@
"@total-typescript/ts-reset": "^0.6.1",
"@types/bun": "latest",
"@types/node": "^24.6.2",
"@types/tar": "^6.1.13",
"@typescript-eslint/parser": "^8.45.0",
"eslint": "^9.37.0",
"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/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/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=="],
"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=="],
@@ -636,8 +633,6 @@
"@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/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=="],
"@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/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=="],
"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=="],
"@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=="],
"@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=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],

View File

@@ -1,20 +1,20 @@
services:
convertx:
build:
context: .
# dockerfile: Debian.Dockerfile
volumes:
- ./data:/app/data
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)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
- 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
# - 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
- 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
ports:
- 3000:3000
services:
convertx:
build:
context: .
# dockerfile: Debian.Dockerfile
volumes:
- ./data:/app/data
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)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
- 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
# - 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
- 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
ports:
- 3000:3000

View File

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

View File

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

View File

@@ -4,12 +4,12 @@
"scripts": {
"dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx",
"format": "run-p 'format:*'",
"format": "npm-run-all 'format:*'",
"format:eslint": "eslint --fix .",
"format:prettier": "prettier --write .",
"build:js": "tsc",
"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:knip": "knip",
"lint:eslint": "eslint .",
@@ -38,7 +38,6 @@
"@total-typescript/ts-reset": "^0.6.1",
"@types/bun": "latest",
"@types/node": "^24.6.2",
"@types/tar": "^6.1.13",
"@typescript-eslint/parser": "^8.45.0",
"eslint": "^9.37.0",
"eslint-plugin-better-tailwindcss": "^3.7.9",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":disableDependencyDashboard"],
"lockFileMaintenance": {
"enabled": true,
"automerge": true
},
"ignoreDeps": ["bun-types", "@types/bun"]
}
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":disableDependencyDashboard"],
"lockFileMaintenance": {
"enabled": true,
"automerge": true
},
"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 { version } from "../../package.json";
export const BaseHtml = ({
children,
title = "ConvertX",
webroot = "",
}: {
children: JSX.Element;
title?: string;
webroot?: string;
}) => (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="webroot" content={webroot} />
<title safe>{title}</title>
<link rel="stylesheet" href={`${webroot}/generated.css`} />
<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="16x16" href={`${webroot}/favicon-16x16.png`} />
<link rel="manifest" href={`${webroot}/site.webmanifest`} />
</head>
<body class={`flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200`}>
{children}
<footer class="w-full">
<div class="p-4 text-center text-sm text-neutral-500">
<span>Powered by </span>
<a
href="https://github.com/C4illin/ConvertX"
class={`
text-neutral-400
hover:text-accent-500
`}
>
ConvertX{" "}
</a>
<span safe>v{version || ""}</span>
</div>
</footer>
</body>
</html>
);
import { Html } from "@elysiajs/html";
import { version } from "../../package.json";
export const BaseHtml = ({
children,
title = "ConvertX",
webroot = "",
}: {
children: JSX.Element;
title?: string;
webroot?: string;
}) => (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="webroot" content={webroot} />
<title safe>{title}</title>
<link rel="stylesheet" href={`${webroot}/generated.css`} />
<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="16x16" href={`${webroot}/favicon-16x16.png`} />
<link rel="manifest" href={`${webroot}/site.webmanifest`} />
</head>
<body class={`flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200`}>
{children}
<footer class="w-full">
<div class="p-4 text-center text-sm text-neutral-500">
<span>Powered by </span>
<a
href="https://github.com/C4illin/ConvertX"
class={`
text-neutral-400
hover:text-accent-500
`}
>
ConvertX{" "}
</a>
<span safe>v{version || ""}</span>
</div>
</footer>
</body>
</html>
);

View File

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

View File

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

View File

@@ -1,86 +1,86 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
document: [
"azw4",
"chm",
"cbr",
"cbz",
"cbt",
"cba",
"cb7",
"djvu",
"docx",
"epub",
"fb2",
"htlz",
"html",
"lit",
"lrf",
"mobi",
"odt",
"pdb",
"pdf",
"pml",
"rb",
"rtf",
"recipe",
"snb",
"tcr",
"txt",
],
},
to: {
document: [
"azw3",
"docx",
"epub",
"fb2",
"html",
"htmlz",
"kepub.epub",
"lit",
"lrf",
"mobi",
"oeb",
"pdb",
"pdf",
"pml",
"rb",
"rtf",
"snb",
"tcr",
"txt",
"txtz",
],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
document: [
"azw4",
"chm",
"cbr",
"cbz",
"cbt",
"cba",
"cb7",
"djvu",
"docx",
"epub",
"fb2",
"htlz",
"html",
"lit",
"lrf",
"mobi",
"odt",
"pdb",
"pdf",
"pml",
"rb",
"rtf",
"recipe",
"snb",
"tcr",
"txt",
],
},
to: {
document: [
"azw3",
"docx",
"epub",
"fb2",
"html",
"htmlz",
"kepub.epub",
"lit",
"lrf",
"mobi",
"oeb",
"pdb",
"pdf",
"pml",
"rb",
"rtf",
"snb",
"tcr",
"txt",
"txtz",
],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,48 +1,48 @@
import fs from "fs";
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
document: ["yaml", "toml", "json", "xml", "csv"],
},
to: {
document: ["yaml", "toml", "json", "csv"],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
const args: string[] = [];
args.push("--file", filePath);
args.push("--read", fileType);
args.push("--write", convertTo);
return new Promise((resolve, reject) => {
execFile("dasel", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
fs.writeFile(targetPath, stdout, (err: NodeJS.ErrnoException | null) => {
if (err) {
reject(`Failed to write output: ${err}`);
} else {
resolve("Done");
}
});
});
});
}
import fs from "fs";
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
document: ["yaml", "toml", "json", "xml", "csv"],
},
to: {
document: ["yaml", "toml", "json", "csv"],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
const args: string[] = [];
args.push("--file", filePath);
args.push("--read", fileType);
args.push("--write", convertTo);
return new Promise((resolve, reject) => {
execFile("dasel", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
fs.writeFile(targetPath, stdout, (err: NodeJS.ErrnoException | null) => {
if (err) {
reject(`Failed to write output: ${err}`);
} else {
resolve("Done");
}
});
});
});
}

View File

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

View File

@@ -1,492 +1,492 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
// declare possible conversions
export const properties = {
from: {
images: [
"3fr",
"3g2",
"3gp",
"aai",
"ai",
"apng",
"art",
"arw",
"avci",
"avi",
"avif",
"avs",
"bayer",
"bayera",
"bgr",
"bgra",
"bgro",
"bmp",
"bmp2",
"bmp3",
"cal",
"cals",
"canvas",
"caption",
"cin",
"clip",
"clipboard",
"cmyk",
"cmyka",
"cr2",
"cr3",
"crw",
"cube",
"cur",
"cut",
"data",
"dcm",
"dcr",
"dcraw",
"dcx",
"dds",
"dfont",
"dng",
"dpx",
"dxt1",
"dxt5",
"emf",
"epdf",
"epi",
"eps",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"erf",
"exr",
"farbfeld",
"fax",
"ff",
"fff",
"file",
"fits",
"fl32",
"flif",
"flv",
"fractal",
"ftp",
"fts",
"ftxt",
"g3",
"g4",
"gif",
"gif87",
"gradient",
"gray",
"graya",
"group4",
"hald",
"hdr",
"heic",
"heif",
"hrz",
"http",
"https",
"icb",
"ico",
"icon",
"iiq",
"inline",
"ipl",
"j2c",
"j2k",
"jng",
"jnx",
"jp2",
"jpc",
"jpe",
"jpeg",
"jpg",
"jpm",
"jps",
"jpt",
"jxl",
"k25",
"kdc",
"label",
"m2v",
"m4v",
"mac",
"map",
"mask",
"mat",
"mdc",
"mef",
"miff",
"mkv",
"mng",
"mono",
"mos",
"mov",
"mp4",
"mpc",
"mpeg",
"mpg",
"mpo",
"mrw",
"msl",
"msvg",
"mtv",
"mvg",
"nef",
"nrw",
"null",
"ora",
"orf",
"otb",
"otf",
"pal",
"palm",
"pam",
"pango",
"pattern",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pdfa",
"pef",
"pes",
"pfa",
"pfb",
"pfm",
"pgm",
"pgx",
"phm",
"picon",
"pict",
"pix",
"pjpeg",
"plasma",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"pocketmod",
"ppm",
"ps",
"psb",
"psd",
"ptif",
"pwp",
"qoi",
"radial",
"raf",
"ras",
"raw",
"rgb",
"rgb565",
"rgba",
"rgbo",
"rgf",
"rla",
"rle",
"rmf",
"rsvg",
"rw2",
"rwl",
"scr",
"screenshot",
"sct",
"sfw",
"sgi",
"six",
"sixel",
"sr2",
"srf",
"srw",
"stegano",
"sti",
"strimg",
"sun",
"svg",
"svgz",
"text",
"tga",
"tiff",
"tiff64",
"tile",
"tim",
"tm2",
"ttc",
"ttf",
"txt",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vips",
"vst",
"wbmp",
"webm",
"webp",
"wmf",
"wmv",
"wpg",
"x3f",
"xbm",
"xc",
"xcf",
"xpm",
"xps",
"xv",
"ycbcr",
"ycbcra",
"yuv",
],
},
to: {
images: [
"aai",
"ai",
"apng",
"art",
"ashlar",
"avif",
"avs",
"bayer",
"bayera",
"bgr",
"bgra",
"bgro",
"bmp",
"bmp2",
"bmp3",
"brf",
"cal",
"cals",
"cin",
"cip",
"clip",
"clipboard",
"cmyk",
"cmyka",
"cur",
"data",
"dcx",
"dds",
"dpx",
"dxt1",
"dxt5",
"epdf",
"epi",
"eps",
"eps2",
"eps3",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"exr",
"farbfeld",
"fax",
"ff",
"fits",
"fl32",
"flif",
"flv",
"fts",
"ftxt",
"g3",
"g4",
"gif",
"gif87",
"gray",
"graya",
"group4",
"hdr",
"histogram",
"hrz",
"htm",
"html",
"icb",
"ico",
"icon",
"info",
"inline",
"ipl",
"isobrl",
"isobrl6",
"j2c",
"j2k",
"jng",
"jp2",
"jpc",
"jpe",
"jpeg",
"jpg",
"jpm",
"jps",
"jpt",
"json",
"jxl",
"m2v",
"m4v",
"map",
"mask",
"mat",
"matte",
"miff",
"mkv",
"mng",
"mono",
"mov",
"mp4",
"mpc",
"mpeg",
"mpg",
"msl",
"msvg",
"mtv",
"mvg",
"null",
"otb",
"pal",
"palm",
"pam",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pdfa",
"pfm",
"pgm",
"pgx",
"phm",
"picon",
"pict",
"pjpeg",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"pocketmod",
"ppm",
"ps",
"ps2",
"ps3",
"psb",
"psd",
"ptif",
"qoi",
"ras",
"rgb",
"rgba",
"rgbo",
"rgf",
"rsvg",
"sgi",
"shtml",
"six",
"sixel",
"sparse",
"strimg",
"sun",
"svg",
"svgz",
"tga",
"thumbnail",
"tiff",
"tiff64",
"txt",
"ubrl",
"ubrl6",
"uil",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vips",
"vst",
"wbmp",
"webm",
"webp",
"wmv",
"wpg",
"xbm",
"xpm",
"xv",
"yaml",
"ycbcr",
"ycbcra",
"yuv",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
let outputArgs: string[] = [];
let inputArgs: string[] = [];
if (convertTo === "ico") {
outputArgs = ["-define", "icon:auto-resize=256,128,64,48,32,16", "-background", "none"];
if (fileType === "svg") {
// this might be a bit too much, but it works
inputArgs = ["-background", "none", "-density", "512"];
}
}
// Handle EMF files specifically to avoid LibreOffice delegate issues
if (fileType === "emf") {
// Use direct conversion without delegates for EMF files
inputArgs.push("-define", "emf:delegate=false", "-density", "300");
outputArgs.push("-background", "white", "-alpha", "remove");
}
return new Promise((resolve, reject) => {
execFile(
"magick",
[...inputArgs, filePath, ...outputArgs, targetPath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
// declare possible conversions
export const properties = {
from: {
images: [
"3fr",
"3g2",
"3gp",
"aai",
"ai",
"apng",
"art",
"arw",
"avci",
"avi",
"avif",
"avs",
"bayer",
"bayera",
"bgr",
"bgra",
"bgro",
"bmp",
"bmp2",
"bmp3",
"cal",
"cals",
"canvas",
"caption",
"cin",
"clip",
"clipboard",
"cmyk",
"cmyka",
"cr2",
"cr3",
"crw",
"cube",
"cur",
"cut",
"data",
"dcm",
"dcr",
"dcraw",
"dcx",
"dds",
"dfont",
"dng",
"dpx",
"dxt1",
"dxt5",
"emf",
"epdf",
"epi",
"eps",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"erf",
"exr",
"farbfeld",
"fax",
"ff",
"fff",
"file",
"fits",
"fl32",
"flif",
"flv",
"fractal",
"ftp",
"fts",
"ftxt",
"g3",
"g4",
"gif",
"gif87",
"gradient",
"gray",
"graya",
"group4",
"hald",
"hdr",
"heic",
"heif",
"hrz",
"http",
"https",
"icb",
"ico",
"icon",
"iiq",
"inline",
"ipl",
"j2c",
"j2k",
"jng",
"jnx",
"jp2",
"jpc",
"jpe",
"jpeg",
"jpg",
"jpm",
"jps",
"jpt",
"jxl",
"k25",
"kdc",
"label",
"m2v",
"m4v",
"mac",
"map",
"mask",
"mat",
"mdc",
"mef",
"miff",
"mkv",
"mng",
"mono",
"mos",
"mov",
"mp4",
"mpc",
"mpeg",
"mpg",
"mpo",
"mrw",
"msl",
"msvg",
"mtv",
"mvg",
"nef",
"nrw",
"null",
"ora",
"orf",
"otb",
"otf",
"pal",
"palm",
"pam",
"pango",
"pattern",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pdfa",
"pef",
"pes",
"pfa",
"pfb",
"pfm",
"pgm",
"pgx",
"phm",
"picon",
"pict",
"pix",
"pjpeg",
"plasma",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"pocketmod",
"ppm",
"ps",
"psb",
"psd",
"ptif",
"pwp",
"qoi",
"radial",
"raf",
"ras",
"raw",
"rgb",
"rgb565",
"rgba",
"rgbo",
"rgf",
"rla",
"rle",
"rmf",
"rsvg",
"rw2",
"rwl",
"scr",
"screenshot",
"sct",
"sfw",
"sgi",
"six",
"sixel",
"sr2",
"srf",
"srw",
"stegano",
"sti",
"strimg",
"sun",
"svg",
"svgz",
"text",
"tga",
"tiff",
"tiff64",
"tile",
"tim",
"tm2",
"ttc",
"ttf",
"txt",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vips",
"vst",
"wbmp",
"webm",
"webp",
"wmf",
"wmv",
"wpg",
"x3f",
"xbm",
"xc",
"xcf",
"xpm",
"xps",
"xv",
"ycbcr",
"ycbcra",
"yuv",
],
},
to: {
images: [
"aai",
"ai",
"apng",
"art",
"ashlar",
"avif",
"avs",
"bayer",
"bayera",
"bgr",
"bgra",
"bgro",
"bmp",
"bmp2",
"bmp3",
"brf",
"cal",
"cals",
"cin",
"cip",
"clip",
"clipboard",
"cmyk",
"cmyka",
"cur",
"data",
"dcx",
"dds",
"dpx",
"dxt1",
"dxt5",
"epdf",
"epi",
"eps",
"eps2",
"eps3",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"exr",
"farbfeld",
"fax",
"ff",
"fits",
"fl32",
"flif",
"flv",
"fts",
"ftxt",
"g3",
"g4",
"gif",
"gif87",
"gray",
"graya",
"group4",
"hdr",
"histogram",
"hrz",
"htm",
"html",
"icb",
"ico",
"icon",
"info",
"inline",
"ipl",
"isobrl",
"isobrl6",
"j2c",
"j2k",
"jng",
"jp2",
"jpc",
"jpe",
"jpeg",
"jpg",
"jpm",
"jps",
"jpt",
"json",
"jxl",
"m2v",
"m4v",
"map",
"mask",
"mat",
"matte",
"miff",
"mkv",
"mng",
"mono",
"mov",
"mp4",
"mpc",
"mpeg",
"mpg",
"msl",
"msvg",
"mtv",
"mvg",
"null",
"otb",
"pal",
"palm",
"pam",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pdfa",
"pfm",
"pgm",
"pgx",
"phm",
"picon",
"pict",
"pjpeg",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"pocketmod",
"ppm",
"ps",
"ps2",
"ps3",
"psb",
"psd",
"ptif",
"qoi",
"ras",
"rgb",
"rgba",
"rgbo",
"rgf",
"rsvg",
"sgi",
"shtml",
"six",
"sixel",
"sparse",
"strimg",
"sun",
"svg",
"svgz",
"tga",
"thumbnail",
"tiff",
"tiff64",
"txt",
"ubrl",
"ubrl6",
"uil",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vips",
"vst",
"wbmp",
"webm",
"webp",
"wmv",
"wpg",
"xbm",
"xpm",
"xv",
"yaml",
"ycbcr",
"ycbcra",
"yuv",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
let outputArgs: string[] = [];
let inputArgs: string[] = [];
if (convertTo === "ico") {
outputArgs = ["-define", "icon:auto-resize=256,128,64,48,32,16", "-background", "none"];
if (fileType === "svg") {
// this might be a bit too much, but it works
inputArgs = ["-background", "none", "-density", "512"];
}
}
// Handle EMF files specifically to avoid LibreOffice delegate issues
if (fileType === "emf") {
// Use direct conversion without delegates for EMF files
inputArgs.push("-define", "emf:delegate=false", "-density", "300");
outputArgs.push("-background", "white", "-alpha", "remove");
}
return new Promise((resolve, reject) => {
execFile(
"magick",
[...inputArgs, filePath, ...outputArgs, targetPath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
});
}

View File

@@ -1,56 +1,56 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
images: ["svg", "pdf", "eps", "ps", "wmf", "emf", "png"],
},
to: {
images: [
"dxf",
"emf",
"eps",
"fxg",
"gpl",
"hpgl",
"html",
"odg",
"pdf",
"png",
"pov",
"ps",
"sif",
"svg",
"svgz",
"tex",
"wmf",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
images: ["svg", "pdf", "eps", "ps", "wmf", "emf", "png"],
},
to: {
images: [
"dxf",
"emf",
"eps",
"fxg",
"gpl",
"hpgl",
"html",
"odg",
"pdf",
"png",
"pov",
"ps",
"sif",
"svg",
"svgz",
"tex",
"wmf",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,38 +1,38 @@
import { execFile as execFileOriginal } from "child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
images: ["avci", "avcs", "avif", "h264", "heic", "heics", "heif", "heifs", "hif", "mkv", "mp4"],
},
to: {
images: ["jpeg", "png", "y4m"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile as execFileOriginal } from "child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
images: ["avci", "avcs", "avif", "h264", "heic", "heics", "heif", "heifs", "hif", "mkv", "mp4"],
},
to: {
images: ["jpeg", "png", "y4m"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,50 +1,50 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
// declare possible conversions
export const properties = {
from: {
jxl: ["jxl"],
images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
},
to: {
jxl: ["apng", "exr", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
images: ["jxl"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
let tool = "";
if (fileType === "jxl") {
tool = "djxl";
}
if (convertTo === "jxl") {
tool = "cjxl";
}
return new Promise((resolve, reject) => {
execFile(tool, [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
// declare possible conversions
export const properties = {
from: {
jxl: ["jxl"],
images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
},
to: {
jxl: ["apng", "exr", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
images: ["jxl"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
let tool = "";
if (fileType === "jxl") {
tool = "djxl";
}
if (convertTo === "jxl") {
tool = "cjxl";
}
return new Promise((resolve, reject) => {
execFile(tool, [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,177 +1,177 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
text: [
"602",
"abw",
"csv",
"cwk",
"doc",
"docm",
"docx",
"dot",
"dotx",
"dotm",
"epub",
"fb2",
"fodt",
"htm",
"html",
"hwp",
"mcw",
"mw",
"mwd",
"lwp",
"lrf",
"odt",
"ott",
"pages",
"pdf",
"psw",
"rtf",
"sdw",
"stw",
"sxw",
"tab",
"tsv",
"txt",
"wn",
"wpd",
"wps",
"wpt",
"wri",
"xhtml",
"xml",
"zabw",
],
},
to: {
text: [
"csv",
"doc",
"docm",
"docx",
"dot",
"dotx",
"dotm",
"epub",
"fodt",
"htm",
"html",
"odt",
"ott",
"pdf",
"rtf",
"tab",
"tsv",
"txt",
"wps",
"wpt",
"xhtml",
"xml",
],
},
};
type FileCategories = "text" | "calc";
const filters: Record<FileCategories, Record<string, string>> = {
text: {
"602": "T602Document",
abw: "AbiWord",
csv: "Text",
doc: "MS Word 97",
docm: "MS Word 2007 XML VBA",
docx: "MS Word 2007 XML",
dot: "MS Word 97 Vorlage",
dotx: "MS Word 2007 XML Template",
dotm: "MS Word 2007 XML Template",
epub: "EPUB",
fb2: "Fictionbook 2",
fodt: "OpenDocument Text Flat XML",
htm: "HTML (StarWriter)",
html: "HTML (StarWriter)",
hwp: "writer_MIZI_Hwp_97",
mcw: "MacWrite",
mw: "MacWrite",
mwd: "Mariner_Write",
lwp: "LotusWordPro",
lrf: "BroadBand eBook",
odt: "writer8",
ott: "writer8_template",
pages: "Apple Pages",
// pdf: "writer_pdf_import",
psw: "PocketWord File",
rtf: "Rich Text Format",
sdw: "StarOffice_Writer",
stw: "writer_StarOffice_XML_Writer_Template",
sxw: "StarOffice XML (Writer)",
tab: "Text",
tsv: "Text",
txt: "Text",
wn: "WriteNow",
wpd: "WordPerfect",
wps: "MS Word 97",
wpt: "MS Word 97 Vorlage",
wri: "MS_Write",
xhtml: "HTML (StarWriter)",
xml: "OpenDocument Text Flat XML",
zabw: "AbiWord",
},
calc: {},
};
const getFilters = (fileType: string, converto: string) => {
if (fileType in filters.text && converto in filters.text) {
return [filters.text[fileType], filters.text[converto]];
} else if (fileType in filters.calc && converto in filters.calc) {
return [filters.calc[fileType], filters.calc[converto]];
}
return [null, null];
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "") ?? targetPath;
// Build arguments array
const args: string[] = [];
args.push("--headless");
const [inFilter, outFilter] = getFilters(fileType, convertTo);
if (inFilter) {
args.push(`--infilter="${inFilter}"`);
}
if (outFilter) {
args.push("--convert-to", `${convertTo}:${outFilter}`, "--outdir", outputPath, filePath);
} else {
args.push("--convert-to", convertTo, "--outdir", outputPath, filePath);
}
return new Promise((resolve, reject) => {
execFile("soffice", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
text: [
"602",
"abw",
"csv",
"cwk",
"doc",
"docm",
"docx",
"dot",
"dotx",
"dotm",
"epub",
"fb2",
"fodt",
"htm",
"html",
"hwp",
"mcw",
"mw",
"mwd",
"lwp",
"lrf",
"odt",
"ott",
"pages",
"pdf",
"psw",
"rtf",
"sdw",
"stw",
"sxw",
"tab",
"tsv",
"txt",
"wn",
"wpd",
"wps",
"wpt",
"wri",
"xhtml",
"xml",
"zabw",
],
},
to: {
text: [
"csv",
"doc",
"docm",
"docx",
"dot",
"dotx",
"dotm",
"epub",
"fodt",
"htm",
"html",
"odt",
"ott",
"pdf",
"rtf",
"tab",
"tsv",
"txt",
"wps",
"wpt",
"xhtml",
"xml",
],
},
};
type FileCategories = "text" | "calc";
const filters: Record<FileCategories, Record<string, string>> = {
text: {
"602": "T602Document",
abw: "AbiWord",
csv: "Text",
doc: "MS Word 97",
docm: "MS Word 2007 XML VBA",
docx: "MS Word 2007 XML",
dot: "MS Word 97 Vorlage",
dotx: "MS Word 2007 XML Template",
dotm: "MS Word 2007 XML Template",
epub: "EPUB",
fb2: "Fictionbook 2",
fodt: "OpenDocument Text Flat XML",
htm: "HTML (StarWriter)",
html: "HTML (StarWriter)",
hwp: "writer_MIZI_Hwp_97",
mcw: "MacWrite",
mw: "MacWrite",
mwd: "Mariner_Write",
lwp: "LotusWordPro",
lrf: "BroadBand eBook",
odt: "writer8",
ott: "writer8_template",
pages: "Apple Pages",
// pdf: "writer_pdf_import",
psw: "PocketWord File",
rtf: "Rich Text Format",
sdw: "StarOffice_Writer",
stw: "writer_StarOffice_XML_Writer_Template",
sxw: "StarOffice XML (Writer)",
tab: "Text",
tsv: "Text",
txt: "Text",
wn: "WriteNow",
wpd: "WordPerfect",
wps: "MS Word 97",
wpt: "MS Word 97 Vorlage",
wri: "MS_Write",
xhtml: "HTML (StarWriter)",
xml: "OpenDocument Text Flat XML",
zabw: "AbiWord",
},
calc: {},
};
const getFilters = (fileType: string, converto: string) => {
if (fileType in filters.text && converto in filters.text) {
return [filters.text[fileType], filters.text[converto]];
} else if (fileType in filters.calc && converto in filters.calc) {
return [filters.calc[fileType], filters.calc[converto]];
}
return [null, null];
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "") ?? targetPath;
// Build arguments array
const args: string[] = [];
args.push("--headless");
const [inFilter, outFilter] = getFilters(fileType, convertTo);
if (inFilter) {
args.push(`--infilter="${inFilter}"`);
}
if (outFilter) {
args.push("--convert-to", `${convertTo}:${outFilter}`, "--outdir", outputPath, filePath);
} else {
args.push("--convert-to", convertTo, "--outdir", outputPath, filePath);
}
return new Promise((resolve, reject) => {
execFile("soffice", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,364 +1,364 @@
import { Cookie } from "elysia";
import db from "../db/db";
import { MAX_CONVERT_PROCESS } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertDasel, properties as propertiesDasel } from "./dasel";
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
import {
convert as convertGraphicsmagick,
properties as propertiesGraphicsmagick,
} from "./graphicsmagick";
import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick";
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
import { convert as convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice";
import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert";
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace";
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
import { convert as convertImage, properties as propertiesImage } from "./vips";
import { convert as convertVtracer, properties as propertiesVtracer } from "./vtracer";
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
const properties: Record<
string,
{
properties: {
from: Record<string, string[]>;
to: Record<string, string[]>;
options?: Record<
string,
Record<
string,
{
description: string;
type: string;
default: number;
}
>
>;
};
converter: (
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
) => unknown;
}
> = {
// Prioritize Inkscape for EMF files as it handles them better than ImageMagick
inkscape: {
properties: propertiesInkscape,
converter: convertInkscape,
},
libjxl: {
properties: propertiesLibjxl,
converter: convertLibjxl,
},
resvg: {
properties: propertiesresvg,
converter: convertresvg,
},
vips: {
properties: propertiesImage,
converter: convertImage,
},
libheif: {
properties: propertiesLibheif,
converter: convertLibheif,
},
xelatex: {
properties: propertiesxelatex,
converter: convertxelatex,
},
calibre: {
properties: propertiesCalibre,
converter: convertCalibre,
},
dasel: {
properties: propertiesDasel,
converter: convertDasel,
},
libreoffice: {
properties: propertiesLibreOffice,
converter: convertLibreOffice,
},
pandoc: {
properties: propertiesPandoc,
converter: convertPandoc,
},
msgconvert: {
properties: propertiesMsgconvert,
converter: convertMsgconvert,
},
dvisvgm: {
properties: propertiesDvisvgm,
converter: convertDvisvgm,
},
imagemagick: {
properties: propertiesImagemagick,
converter: convertImagemagick,
},
graphicsmagick: {
properties: propertiesGraphicsmagick,
converter: convertGraphicsmagick,
},
assimp: {
properties: propertiesassimp,
converter: convertassimp,
},
ffmpeg: {
properties: propertiesFFmpeg,
converter: convertFFmpeg,
},
potrace: {
properties: propertiesPotrace,
converter: convertPotrace,
},
vtracer: {
properties: propertiesVtracer,
converter: convertVtracer,
},
};
function chunks<T>(arr: T[], size: number): T[][] {
if (size <= 0) {
return [arr];
}
return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) =>
arr.slice(i * size, i * size + size),
);
}
export async function handleConvert(
fileNames: string[],
userUploadsDir: string,
userOutputDir: string,
convertTo: string,
converterName: string,
jobId: Cookie<string | undefined>,
) {
const query = db.query(
"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)) {
const toProcess: Promise<string>[] = [];
for (const fileName of chunk) {
const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop() ?? "";
const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo);
const newFileName = fileName.replace(
new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
newFileExt,
);
const targetPath = `${userOutputDir}${newFileName}`;
toProcess.push(
new Promise((resolve, reject) => {
mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
.then((r) => {
if (jobId.value) {
query.run(jobId.value, fileName, newFileName, r);
}
resolve(r);
})
.catch((c) => reject(c));
}),
);
}
await Promise.all(toProcess);
}
}
async function mainConverter(
inputFilePath: string,
fileTypeOriginal: string,
convertTo: string,
targetPath: string,
options?: unknown,
converterName?: string,
) {
const fileType = normalizeFiletype(fileTypeOriginal);
let converterFunc: (typeof properties)["libjxl"]["converter"] | undefined;
if (converterName) {
converterFunc = properties[converterName]?.converter;
} else {
// Iterate over each converter in properties
for (converterName in properties) {
const converterObj = properties[converterName];
if (!converterObj) {
break;
}
for (const key in converterObj.properties.from) {
if (
converterObj?.properties?.from[key]?.includes(fileType) &&
converterObj?.properties?.to[key]?.includes(convertTo)
) {
converterFunc = converterObj.converter;
break;
}
}
}
}
if (!converterFunc) {
console.log(`No available converter supports converting from ${fileType} to ${convertTo}.`);
return "File type not supported";
}
try {
const result = await converterFunc(inputFilePath, fileType, convertTo, targetPath, options);
console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`,
result,
);
if (typeof result === "string") {
return result;
}
return "Done";
} catch (error) {
console.error(
`Failed to convert ${inputFilePath} from ${fileType} to ${convertTo} using ${converterName}.`,
error,
);
return "Failed, check logs";
}
}
const possibleTargets: Record<string, Record<string, string[]>> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.from) {
if (converterProperties.from[key] === undefined) {
continue;
}
for (const extension of converterProperties.from[key] ?? []) {
if (!possibleTargets[extension]) {
possibleTargets[extension] = {};
}
possibleTargets[extension][converterName] = converterProperties.to[key] || [];
}
}
}
export const getPossibleTargets = (from: string): Record<string, string[]> => {
const fromClean = normalizeFiletype(from);
return possibleTargets[fromClean] || {};
};
const possibleInputs: string[] = [];
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.from) {
for (const extension of converterProperties.from[key] ?? []) {
if (!possibleInputs.includes(extension)) {
possibleInputs.push(extension);
}
}
}
}
possibleInputs.sort();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getPossibleInputs = () => {
return possibleInputs;
};
const allTargets: Record<string, string[]> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.to) {
if (allTargets[converterName]) {
allTargets[converterName].push(...(converterProperties.to[key] || []));
} else {
allTargets[converterName] = converterProperties.to[key] || [];
}
}
}
export const getAllTargets = () => {
return allTargets;
};
const allInputs: Record<string, string[]> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.from) {
if (allInputs[converterName]) {
allInputs[converterName].push(...(converterProperties.from[key] || []));
} else {
allInputs[converterName] = converterProperties.from[key] || [];
}
}
}
export const getAllInputs = (converter: string) => {
return allInputs[converter] || [];
};
// // count the number of unique formats
// const uniqueFormats = new Set();
// for (const converterName in properties) {
// const converterProperties = properties[converterName]?.properties;
// if (!converterProperties) {
// continue;
// }
// for (const key in converterProperties.from) {
// for (const extension of converterProperties.from[key] ?? []) {
// uniqueFormats.add(extension);
// }
// }
// for (const key in converterProperties.to) {
// for (const extension of converterProperties.to[key] ?? []) {
// uniqueFormats.add(extension);
// }
// }
// }
// // print the number of unique Inputs and Outputs
// console.log(`Unique Formats: ${uniqueFormats.size}`);
import { Cookie } from "elysia";
import db from "../db/db";
import { MAX_CONVERT_PROCESS } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertDasel, properties as propertiesDasel } from "./dasel";
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
import {
convert as convertGraphicsmagick,
properties as propertiesGraphicsmagick,
} from "./graphicsmagick";
import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick";
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
import { convert as convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice";
import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert";
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace";
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
import { convert as convertImage, properties as propertiesImage } from "./vips";
import { convert as convertVtracer, properties as propertiesVtracer } from "./vtracer";
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
const properties: Record<
string,
{
properties: {
from: Record<string, string[]>;
to: Record<string, string[]>;
options?: Record<
string,
Record<
string,
{
description: string;
type: string;
default: number;
}
>
>;
};
converter: (
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
) => unknown;
}
> = {
// Prioritize Inkscape for EMF files as it handles them better than ImageMagick
inkscape: {
properties: propertiesInkscape,
converter: convertInkscape,
},
libjxl: {
properties: propertiesLibjxl,
converter: convertLibjxl,
},
resvg: {
properties: propertiesresvg,
converter: convertresvg,
},
vips: {
properties: propertiesImage,
converter: convertImage,
},
libheif: {
properties: propertiesLibheif,
converter: convertLibheif,
},
xelatex: {
properties: propertiesxelatex,
converter: convertxelatex,
},
calibre: {
properties: propertiesCalibre,
converter: convertCalibre,
},
dasel: {
properties: propertiesDasel,
converter: convertDasel,
},
libreoffice: {
properties: propertiesLibreOffice,
converter: convertLibreOffice,
},
pandoc: {
properties: propertiesPandoc,
converter: convertPandoc,
},
msgconvert: {
properties: propertiesMsgconvert,
converter: convertMsgconvert,
},
dvisvgm: {
properties: propertiesDvisvgm,
converter: convertDvisvgm,
},
imagemagick: {
properties: propertiesImagemagick,
converter: convertImagemagick,
},
graphicsmagick: {
properties: propertiesGraphicsmagick,
converter: convertGraphicsmagick,
},
assimp: {
properties: propertiesassimp,
converter: convertassimp,
},
ffmpeg: {
properties: propertiesFFmpeg,
converter: convertFFmpeg,
},
potrace: {
properties: propertiesPotrace,
converter: convertPotrace,
},
vtracer: {
properties: propertiesVtracer,
converter: convertVtracer,
},
};
function chunks<T>(arr: T[], size: number): T[][] {
if (size <= 0) {
return [arr];
}
return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) =>
arr.slice(i * size, i * size + size),
);
}
export async function handleConvert(
fileNames: string[],
userUploadsDir: string,
userOutputDir: string,
convertTo: string,
converterName: string,
jobId: Cookie<string | undefined>,
) {
const query = db.query(
"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)) {
const toProcess: Promise<string>[] = [];
for (const fileName of chunk) {
const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop() ?? "";
const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo);
const newFileName = fileName.replace(
new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
newFileExt,
);
const targetPath = `${userOutputDir}${newFileName}`;
toProcess.push(
new Promise((resolve, reject) => {
mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
.then((r) => {
if (jobId.value) {
query.run(jobId.value, fileName, newFileName, r);
}
resolve(r);
})
.catch((c) => reject(c));
}),
);
}
await Promise.all(toProcess);
}
}
async function mainConverter(
inputFilePath: string,
fileTypeOriginal: string,
convertTo: string,
targetPath: string,
options?: unknown,
converterName?: string,
) {
const fileType = normalizeFiletype(fileTypeOriginal);
let converterFunc: (typeof properties)["libjxl"]["converter"] | undefined;
if (converterName) {
converterFunc = properties[converterName]?.converter;
} else {
// Iterate over each converter in properties
for (converterName in properties) {
const converterObj = properties[converterName];
if (!converterObj) {
break;
}
for (const key in converterObj.properties.from) {
if (
converterObj?.properties?.from[key]?.includes(fileType) &&
converterObj?.properties?.to[key]?.includes(convertTo)
) {
converterFunc = converterObj.converter;
break;
}
}
}
}
if (!converterFunc) {
console.log(`No available converter supports converting from ${fileType} to ${convertTo}.`);
return "File type not supported";
}
try {
const result = await converterFunc(inputFilePath, fileType, convertTo, targetPath, options);
console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`,
result,
);
if (typeof result === "string") {
return result;
}
return "Done";
} catch (error) {
console.error(
`Failed to convert ${inputFilePath} from ${fileType} to ${convertTo} using ${converterName}.`,
error,
);
return "Failed, check logs";
}
}
const possibleTargets: Record<string, Record<string, string[]>> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.from) {
if (converterProperties.from[key] === undefined) {
continue;
}
for (const extension of converterProperties.from[key] ?? []) {
if (!possibleTargets[extension]) {
possibleTargets[extension] = {};
}
possibleTargets[extension][converterName] = converterProperties.to[key] || [];
}
}
}
export const getPossibleTargets = (from: string): Record<string, string[]> => {
const fromClean = normalizeFiletype(from);
return possibleTargets[fromClean] || {};
};
const possibleInputs: string[] = [];
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.from) {
for (const extension of converterProperties.from[key] ?? []) {
if (!possibleInputs.includes(extension)) {
possibleInputs.push(extension);
}
}
}
}
possibleInputs.sort();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getPossibleInputs = () => {
return possibleInputs;
};
const allTargets: Record<string, string[]> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.to) {
if (allTargets[converterName]) {
allTargets[converterName].push(...(converterProperties.to[key] || []));
} else {
allTargets[converterName] = converterProperties.to[key] || [];
}
}
}
export const getAllTargets = () => {
return allTargets;
};
const allInputs: Record<string, string[]> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.from) {
if (allInputs[converterName]) {
allInputs[converterName].push(...(converterProperties.from[key] || []));
} else {
allInputs[converterName] = converterProperties.from[key] || [];
}
}
}
export const getAllInputs = (converter: string) => {
return allInputs[converter] || [];
};
// // count the number of unique formats
// const uniqueFormats = new Set();
// for (const converterName in properties) {
// const converterProperties = properties[converterName]?.properties;
// if (!converterProperties) {
// continue;
// }
// for (const key in converterProperties.from) {
// for (const extension of converterProperties.from[key] ?? []) {
// uniqueFormats.add(extension);
// }
// }
// for (const key in converterProperties.to) {
// for (const extension of converterProperties.to[key] ?? []) {
// uniqueFormats.add(extension);
// }
// }
// }
// // print the number of unique Inputs and Outputs
// console.log(`Unique Formats: ${uniqueFormats.size}`);

View File

@@ -1,52 +1,52 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
email: ["msg"],
},
to: {
email: ["eml"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
return new Promise((resolve, reject) => {
if (fileType === "msg" && convertTo === "eml") {
// Convert MSG to EML using msgconvert
// msgconvert will output to the same directory as the input file with .eml extension
// We need to use --outfile to specify the target path
const args = ["--outfile", targetPath, filePath];
execFile("msgconvert", args, (error, stdout, stderr) => {
if (error) {
reject(new Error(`msgconvert failed: ${error.message}`));
return;
}
if (stderr) {
// Log sanitized stderr to avoid exposing sensitive paths
const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]");
console.warn(
`msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`,
);
}
resolve(targetPath);
});
} else {
reject(
new Error(
`Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`,
),
);
}
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
email: ["msg"],
},
to: {
email: ["eml"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
return new Promise((resolve, reject) => {
if (fileType === "msg" && convertTo === "eml") {
// Convert MSG to EML using msgconvert
// msgconvert will output to the same directory as the input file with .eml extension
// We need to use --outfile to specify the target path
const args = ["--outfile", targetPath, filePath];
execFile("msgconvert", args, (error, stdout, stderr) => {
if (error) {
reject(new Error(`msgconvert failed: ${error.message}`));
return;
}
if (stderr) {
// Log sanitized stderr to avoid exposing sensitive paths
const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]");
console.warn(
`msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`,
);
}
resolve(targetPath);
});
} else {
reject(
new Error(
`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 { ExecFileFn } from "./types";
export const properties = {
from: {
text: [
"textile",
"tikiwiki",
"tsv",
"twiki",
"typst",
"vimwiki",
"biblatex",
"bibtex",
"bits",
"commonmark",
"commonmark_x",
"creole",
"csljson",
"csv",
"djot",
"docbook",
"docx",
"dokuwiki",
"endnotexml",
"epub",
"fb2",
"gfm",
"haddock",
"html",
"ipynb",
"jats",
"jira",
"json",
"latex",
"man",
"markdown",
"markdown_mmd",
"markdown_phpextra",
"markdown_strict",
"mediawiki",
"muse",
"pandoc native",
"opml",
"org",
"ris",
"rst",
"rtf",
"t2t",
],
},
to: {
text: [
"tei",
"texinfo",
"textile",
"typst",
"xwiki",
"zimwiki",
"asciidoc",
"asciidoc_legacy",
"asciidoctor",
"beamer",
"biblatex",
"bibtex",
"chunkedhtml",
"commonmark",
"commonmark_x",
"context",
"csljson",
"djot",
"docbook",
"docbook4",
"docbook5",
"docx",
"dokuwiki",
"dzslides",
"epub",
"epub2",
"epub3",
"fb2",
"gfm",
"haddock",
"html",
"html4",
"html5",
"icml",
"ipynb",
"jats",
"jats_archiving",
"jats_articleauthoring",
"jats_publishing",
"jira",
"json",
"latex",
"man",
"markdown",
"markdown_mmd",
"markdown_phpextra",
"markdown_strict",
"markua",
"mediawiki",
"ms",
"muse",
"pandoc native",
"odt",
"opendocument",
"opml",
"org",
"pdf",
"plain",
"pptx",
"revealjs",
"rst",
"rtf",
"s5",
"slideous",
"slidy",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
// set xelatex here
const xelatex = ["pdf", "latex"];
// Build arguments array
const args: string[] = [];
if (xelatex.includes(convertTo)) {
args.push("--pdf-engine=xelatex");
}
args.push(filePath);
args.push("-f", fileType);
args.push("-t", convertTo);
args.push("-o", targetPath);
return new Promise((resolve, reject) => {
execFile("pandoc", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
text: [
"textile",
"tikiwiki",
"tsv",
"twiki",
"typst",
"vimwiki",
"biblatex",
"bibtex",
"bits",
"commonmark",
"commonmark_x",
"creole",
"csljson",
"csv",
"djot",
"docbook",
"docx",
"dokuwiki",
"endnotexml",
"epub",
"fb2",
"gfm",
"haddock",
"html",
"ipynb",
"jats",
"jira",
"json",
"latex",
"man",
"markdown",
"markdown_mmd",
"markdown_phpextra",
"markdown_strict",
"mediawiki",
"muse",
"pandoc native",
"opml",
"org",
"ris",
"rst",
"rtf",
"t2t",
],
},
to: {
text: [
"tei",
"texinfo",
"textile",
"typst",
"xwiki",
"zimwiki",
"asciidoc",
"asciidoc_legacy",
"asciidoctor",
"beamer",
"biblatex",
"bibtex",
"chunkedhtml",
"commonmark",
"commonmark_x",
"context",
"csljson",
"djot",
"docbook",
"docbook4",
"docbook5",
"docx",
"dokuwiki",
"dzslides",
"epub",
"epub2",
"epub3",
"fb2",
"gfm",
"haddock",
"html",
"html4",
"html5",
"icml",
"ipynb",
"jats",
"jats_archiving",
"jats_articleauthoring",
"jats_publishing",
"jira",
"json",
"latex",
"man",
"markdown",
"markdown_mmd",
"markdown_phpextra",
"markdown_strict",
"markua",
"mediawiki",
"ms",
"muse",
"pandoc native",
"odt",
"opendocument",
"opml",
"org",
"pdf",
"plain",
"pptx",
"revealjs",
"rst",
"rtf",
"s5",
"slideous",
"slidy",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
// set xelatex here
const xelatex = ["pdf", "latex"];
// Build arguments array
const args: string[] = [];
if (xelatex.includes(convertTo)) {
args.push("--pdf-engine=xelatex");
}
args.push(filePath);
args.push("-f", fileType);
args.push("-t", convertTo);
args.push("-o", targetPath);
return new Promise((resolve, reject) => {
execFile("pandoc", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,50 +1,50 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
images: ["pnm", "pbm", "pgm", "bmp"],
},
to: {
images: [
"svg",
"pdf",
"pdfpage",
"eps",
"postscript",
"ps",
"dxf",
"geojson",
"pgm",
"gimppath",
"xfig",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
images: ["pnm", "pbm", "pgm", "bmp"],
},
to: {
images: [
"svg",
"pdf",
"pdfpage",
"eps",
"postscript",
"ps",
"dxf",
"geojson",
"pgm",
"gimppath",
"xfig",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,38 +1,38 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
images: ["svg"],
},
to: {
images: ["png"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
images: ["svg"],
},
to: {
images: ["png"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal, // to make it mockable
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,17 +1,17 @@
import { ExecFileOptions } from "child_process";
export type ExecFileFn = (
cmd: string,
args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void,
options?: ExecFileOptions,
) => void;
export type ConvertFnWithExecFile = (
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options: unknown,
execFileOverride?: ExecFileFn,
) => Promise<string>;
import { ExecFileOptions } from "child_process";
export type ExecFileFn = (
cmd: string,
args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void,
options?: ExecFileOptions,
) => void;
export type ConvertFnWithExecFile = (
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options: unknown,
execFileOverride?: ExecFileFn,
) => Promise<string>;

View File

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

View File

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

View File

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

View File

@@ -1,41 +1,41 @@
import { Database } from "bun:sqlite";
const db = new Database("./data/mydb.sqlite", { create: true });
if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL,
file_name TEXT NOT NULL,
output_file_name TEXT NOT NULL,
status TEXT DEFAULT 'not started',
FOREIGN KEY (job_id) REFERENCES jobs(id)
);
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date_created TEXT NOT NULL,
status TEXT DEFAULT 'not started',
num_files INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
);
PRAGMA user_version = 1;`);
}
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
if (dbVersion === 0) {
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
db.exec("PRAGMA user_version = 1;");
console.log("Updated database to version 1.");
}
// enable WAL mode
db.exec("PRAGMA journal_mode = WAL;");
export default db;
import { Database } from "bun:sqlite";
const db = new Database("./data/mydb.sqlite", { create: true });
if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL,
file_name TEXT NOT NULL,
output_file_name TEXT NOT NULL,
status TEXT DEFAULT 'not started',
FOREIGN KEY (job_id) REFERENCES jobs(id)
);
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date_created TEXT NOT NULL,
status TEXT DEFAULT 'not started',
num_files INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
);
PRAGMA user_version = 1;`);
}
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
if (dbVersion === 0) {
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
db.exec("PRAGMA user_version = 1;");
console.log("Updated database to version 1.");
}
// enable WAL mode
db.exec("PRAGMA journal_mode = WAL;");
export default db;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,32 @@
@import "./theme/theme.css";
@import "tailwindcss";
@plugin "tailwind-scrollbar";
@theme {
--color-contrast: var(--contrast);
--color-neutral-900: var(--neutral-900);
--color-neutral-800: var(--neutral-800);
--color-neutral-700: var(--neutral-700);
--color-neutral-600: var(--neutral-600);
--color-neutral-500: var(--neutral-500);
--color-neutral-400: var(--neutral-400);
--color-neutral-300: var(--neutral-300);
--color-neutral-200: var(--neutral-200);
--color-neutral-100: var(--neutral-100);
--color-accent-600: var(--accent-600);
--color-accent-500: var(--accent-500);
--color-accent-400: var(--accent-400);
}
@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;
}
@utility btn-primary {
@apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors;
}
@utility btn-secondary {
@apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors;
}
@import "./theme/theme.css";
@import "tailwindcss";
@plugin "tailwind-scrollbar";
@theme {
--color-contrast: var(--contrast);
--color-neutral-900: var(--neutral-900);
--color-neutral-800: var(--neutral-800);
--color-neutral-700: var(--neutral-700);
--color-neutral-600: var(--neutral-600);
--color-neutral-500: var(--neutral-500);
--color-neutral-400: var(--neutral-400);
--color-neutral-300: var(--neutral-300);
--color-neutral-200: var(--neutral-200);
--color-neutral-100: var(--neutral-100);
--color-accent-600: var(--accent-600);
--color-accent-500: var(--accent-500);
--color-accent-400: var(--accent-400);
}
@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;
}
@utility btn-primary {
@apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors;
}
@utility btn-secondary {
@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 Elysia, { t } from "elysia";
import { getPossibleTargets } from "../converters/main";
import { userService } from "./user";
export const chooseConverter = new Elysia().use(userService).post(
"/conversions",
({ body }) => {
return (
<>
<article
class={`
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
sm:h-[30vh]
`}
>
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
<article
class={`convert_to_group flex w-full flex-col border-b border-neutral-700 p-4`}
data-converter={converter}
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
{targets.map((target) => (
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
))}
</article>
<select name="convert_to" aria-label="Convert to" required hidden>
<option selected disabled value="">
Convert to
</option>
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
<optgroup label={converter}>
{targets.map((target) => (
<option value={`${target},${converter}`} safe>
{target}
</option>
))}
</optgroup>
))}
</select>
</>
);
},
{ body: t.Object({ fileType: t.String() }) },
);
import { Html } from "@elysiajs/html";
import Elysia, { t } from "elysia";
import { getPossibleTargets } from "../converters/main";
import { userService } from "./user";
export const chooseConverter = new Elysia().use(userService).post(
"/conversions",
({ body }) => {
return (
<>
<article
class={`
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
sm:h-[30vh]
`}
>
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
<article
class={`convert_to_group flex w-full flex-col border-b border-neutral-700 p-4`}
data-converter={converter}
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
{targets.map((target) => (
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
))}
</article>
<select name="convert_to" aria-label="Convert to" required hidden>
<option selected disabled value="">
Convert to
</option>
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
<optgroup label={converter}>
{targets.map((target) => (
<option value={`${target},${converter}`} safe>
{target}
</option>
))}
</optgroup>
))}
</select>
</>
);
},
{ body: t.Object({ fileType: t.String() }) },
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,73 +1,75 @@
import { Html } from "@elysiajs/html";
import Elysia from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import { getAllInputs, getAllTargets } from "../converters/main";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const listConverters = new Elysia()
.use(userService)
.get("/converters", async () => {
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
<>
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<h1 class="mb-4 text-xl">Converters</h1>
<table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
[&_ul]:list-inside [&_ul]:list-disc
`}
>
<thead>
<tr>
<th class="mx-4 my-2">Converter</th>
<th class="mx-4 my-2">From (Count)</th>
<th class="mx-4 my-2">To (Count)</th>
</tr>
</thead>
<tbody>
{Object.entries(getAllTargets()).map(([converter, targets]) => {
const inputs = getAllInputs(converter);
return (
<tr>
<td safe>{converter}</td>
<td>
Count: {inputs.length}
<ul>
{inputs.map((input) => (
<li safe>{input}</li>
))}
</ul>
</td>
<td>
Count: {targets.length}
<ul>
{targets.map((target) => (
<li safe>{target}</li>
))}
</ul>
</td>
</tr>
);
})}
</tbody>
</table>
</article>
</main>
</>
</BaseHtml>
);
}, {
auth: true
});
import { Html } from "@elysiajs/html";
import Elysia from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import { getAllInputs, getAllTargets } from "../converters/main";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const listConverters = new Elysia().use(userService).get(
"/converters",
async () => {
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
<>
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<h1 class="mb-4 text-xl">Converters</h1>
<table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
[&_ul]:list-inside [&_ul]:list-disc
`}
>
<thead>
<tr>
<th class="mx-4 my-2">Converter</th>
<th class="mx-4 my-2">From (Count)</th>
<th class="mx-4 my-2">To (Count)</th>
</tr>
</thead>
<tbody>
{Object.entries(getAllTargets()).map(([converter, targets]) => {
const inputs = getAllInputs(converter);
return (
<tr>
<td safe>{converter}</td>
<td>
Count: {inputs.length}
<ul>
{inputs.map((input) => (
<li safe>{input}</li>
))}
</ul>
</td>
<td>
Count: {targets.length}
<ul>
{targets.map((target) => (
<li safe>{target}</li>
))}
</ul>
</td>
</tr>
);
})}
</tbody>
</table>
</article>
</main>
</>
</BaseHtml>
);
},
{
auth: true,
},
);

View File

@@ -1,207 +1,215 @@
import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user";
function ResultsArticle({
user,
job,
files,
outputPath,
}: {
user: {
id: string;
} & JWTPayloadSpec;
job: Jobs;
files: Filename[];
outputPath: string;
}) {
return (
<article class="article">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl">Results</h1>
<div>
<a
style={files.length !== job.num_files ? "pointer-events: none;" : ""}
href={`${WEBROOT}/archive/${user.id}/${job.id}`}
download={`converted_files_${job.id}.tar`}
>
<button
type="button"
class="float-right w-40 btn-primary"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
>
{files.length === job.num_files ? "Download All" : "Converting..."}
</button>
</a>
</div>
</div>
<progress
max={job.num_files}
value={files.length}
class={`
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
[&::-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]
`}
/>
<table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead>
<tr>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Converted File Name
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Status
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
View
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Download
</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr>
<td safe class="max-w-[20vw] truncate">
{file.output_file_name}
</td>
<td safe>{file.status}</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
>
View
</a>
</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}
>
Download
</a>
</td>
</tr>
))}
</tbody>
</table>
</article>
);
}
export const results = new Elysia()
.use(userService)
.get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { job_id }, user }) => {
if (job_id?.value) {
// Clear the job_id cookie since we are viewing the results
job_id.remove();
}
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
if (!job) {
set.status = 404;
return {
message: "Job not found.",
};
}
const outputPath = `${user.id}/${params.jobId}/`;
const files = db
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
.all(params.jobId);
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
<>
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
<main
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 />
</>
</BaseHtml>
);
}, { auth: true })
.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
job_id.remove();
}
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
if (!job) {
set.status = 404;
return {
message: "Job not found.",
};
}
const outputPath = `${user.id}/${params.jobId}/`;
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 });
import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user";
function ResultsArticle({
user,
job,
files,
outputPath,
}: {
user: {
id: string;
} & JWTPayloadSpec;
job: Jobs;
files: Filename[];
outputPath: string;
}) {
return (
<article class="article">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl">Results</h1>
<div>
<a
style={files.length !== job.num_files ? "pointer-events: none;" : ""}
href={`${WEBROOT}/archive/${user.id}/${job.id}`}
download={`converted_files_${job.id}.tar`}
>
<button
type="button"
class="float-right w-40 btn-primary"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
>
{files.length === job.num_files ? "Download All" : "Converting..."}
</button>
</a>
</div>
</div>
<progress
max={job.num_files}
value={files.length}
class={`
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
[&::-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]
`}
/>
<table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead>
<tr>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Converted File Name
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Status
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
View
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Download
</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr>
<td safe class="max-w-[20vw] truncate">
{file.output_file_name}
</td>
<td safe>{file.status}</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
>
View
</a>
</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}
>
Download
</a>
</td>
</tr>
))}
</tbody>
</table>
</article>
);
}
export const results = new Elysia()
.use(userService)
.get(
"/results/:jobId",
async ({ params, set, cookie: { job_id }, user }) => {
if (job_id?.value) {
// Clear the job_id cookie since we are viewing the results
job_id.remove();
}
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
if (!job) {
set.status = 404;
return {
message: "Job not found.",
};
}
const outputPath = `${user.id}/${params.jobId}/`;
const files = db
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
.all(params.jobId);
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
<>
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
<main
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 />
</>
</BaseHtml>
);
},
{ auth: true },
)
.post(
"/progress/:jobId",
async ({ set, params, cookie: { job_id }, user }) => {
if (job_id?.value) {
// Clear the job_id cookie since we are viewing the results
job_id.remove();
}
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
if (!job) {
set.status = 404;
return {
message: "Job not found.",
};
}
const outputPath = `${user.id}/${params.jobId}/`;
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 { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia, t } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import { getAllTargets } from "../converters/main";
import db from "../db/db";
import { User } from "../db/types";
import {
ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED,
HIDE_HISTORY,
HTTP_ALLOWED,
UNAUTHENTICATED_USER_SHARING,
WEBROOT,
} from "../helpers/env";
import { FIRST_RUN, userService } from "./user";
export const root = new Elysia()
.use(userService)
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
if (!ALLOW_UNAUTHENTICATED) {
if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302);
}
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
}
// validate jwt
let user: ({ id: string } & JWTPayloadSpec) | false = false;
if (ALLOW_UNAUTHENTICATED) {
const newUserId = String(
UNAUTHENTICATED_USER_SHARING
? 0
: randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
);
const accessToken = await jwt.sign({
id: newUserId,
});
user = { id: newUserId };
if (!auth) {
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
};
}
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
} else if (auth?.value) {
user = await jwt.verify(auth.value);
if (
user !== false &&
user.id &&
(Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
) {
// Make sure user exists in db
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
if (!existingUser) {
if (auth?.value) {
auth.remove();
}
return redirect(`${WEBROOT}/login`, 302);
}
}
}
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
// create a new job
db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
user.id,
new Date().toISOString(),
);
const { id } = db
.query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
.get(user.id) as { id: number };
if (!jobId) {
return { message: "Cookies should be enabled to use this app." };
}
jobId.set({
value: id,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
console.log("jobId set to:", id);
return (
<BaseHtml webroot={WEBROOT}>
<>
<Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
loggedIn
/>
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<h1 class="mb-4 text-xl">Convert</h1>
<div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto">
<table
id="file-list"
class={`
w-full table-auto rounded bg-neutral-900
[&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
/>
</div>
<div
id="dropzone"
class={`
relative flex h-48 w-full items-center justify-center rounded border border-dashed
border-neutral-700 transition-all
hover:border-neutral-600
[&.dragover]:border-4 [&.dragover]:border-neutral-500
`}
>
<span>
<b>Choose a file</b> or drag it here
</span>
<input
type="file"
name="file"
multiple
class="absolute inset-0 size-full cursor-pointer opacity-0"
/>
</div>
</article>
<form
method="post"
action={`${WEBROOT}/convert`}
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
>
<input type="hidden" name="file_names" id="file_names" />
<article class="article w-full">
<input
type="search"
name="convert_to_search"
placeholder="Search for conversions"
autocomplete="off"
class="w-full rounded-sm bg-neutral-800 p-4"
/>
<div class="select_container relative">
<article
class={`
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
sm:h-[30vh]
`}
>
{Object.entries(getAllTargets()).map(([converter, targets]) => (
<article
class={`
convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
`}
data-converter={converter}
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul class={`convert_to_target flex flex-row flex-wrap gap-1`}>
{targets.map((target) => (
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
))}
</article>
{/* 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>
<option selected disabled value="">
Convert to
</option>
{Object.entries(getAllTargets()).map(([converter, targets]) => (
<optgroup label={converter}>
{targets.map((target) => (
<option value={`${target},${converter}`} safe>
{target}
</option>
))}
</optgroup>
))}
</select>
</div>
</article>
<input
class={`
w-full btn-primary opacity-100
disabled:cursor-not-allowed disabled:opacity-50
`}
type="submit"
value="Convert"
disabled
/>
</form>
</main>
<script src="script.js" defer />
</>
</BaseHtml>
);
}, {
cookie: t.Cookie({
auth: t.Optional(t.String()),
jobId: t.Optional(t.String()),
})
});
import { randomInt } from "node:crypto";
import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia, t } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import { getAllTargets } from "../converters/main";
import db from "../db/db";
import { User } from "../db/types";
import {
ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED,
HIDE_HISTORY,
HTTP_ALLOWED,
UNAUTHENTICATED_USER_SHARING,
WEBROOT,
} from "../helpers/env";
import { FIRST_RUN, userService } from "./user";
export const root = new Elysia().use(userService).get(
"/",
async ({ jwt, redirect, cookie: { auth, jobId } }) => {
if (!ALLOW_UNAUTHENTICATED) {
if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302);
}
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
}
// validate jwt
let user: ({ id: string } & JWTPayloadSpec) | false = false;
if (ALLOW_UNAUTHENTICATED) {
const newUserId = String(
UNAUTHENTICATED_USER_SHARING
? 0
: randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
);
const accessToken = await jwt.sign({
id: newUserId,
});
user = { id: newUserId };
if (!auth) {
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
};
}
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
} else if (auth?.value) {
user = await jwt.verify(auth.value);
if (
user !== false &&
user.id &&
(Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
) {
// Make sure user exists in db
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
if (!existingUser) {
if (auth?.value) {
auth.remove();
}
return redirect(`${WEBROOT}/login`, 302);
}
}
}
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
// create a new job
db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
user.id,
new Date().toISOString(),
);
const { id } = db
.query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
.get(user.id) as { id: number };
if (!jobId) {
return { message: "Cookies should be enabled to use this app." };
}
jobId.set({
value: id,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
console.log("jobId set to:", id);
return (
<BaseHtml webroot={WEBROOT}>
<>
<Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
loggedIn
/>
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<h1 class="mb-4 text-xl">Convert</h1>
<div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto">
<table
id="file-list"
class={`
w-full table-auto rounded bg-neutral-900
[&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
/>
</div>
<div
id="dropzone"
class={`
relative flex h-48 w-full items-center justify-center rounded border border-dashed
border-neutral-700 transition-all
hover:border-neutral-600
[&.dragover]:border-4 [&.dragover]:border-neutral-500
`}
>
<span>
<b>Choose a file</b> or drag it here
</span>
<input
type="file"
name="file"
multiple
class="absolute inset-0 size-full cursor-pointer opacity-0"
/>
</div>
</article>
<form
method="post"
action={`${WEBROOT}/convert`}
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
>
<input type="hidden" name="file_names" id="file_names" />
<article class="article w-full">
<input
type="search"
name="convert_to_search"
placeholder="Search for conversions"
autocomplete="off"
class="w-full rounded-sm bg-neutral-800 p-4"
/>
<div class="select_container relative">
<article
class={`
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
sm:h-[30vh]
`}
>
{Object.entries(getAllTargets()).map(([converter, targets]) => (
<article
class={`
convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
`}
data-converter={converter}
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul class={`convert_to_target flex flex-row flex-wrap gap-1`}>
{targets.map((target) => (
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
))}
</article>
{/* 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>
<option selected disabled value="">
Convert to
</option>
{Object.entries(getAllTargets()).map(([converter, targets]) => (
<optgroup label={converter}>
{targets.map((target) => (
<option value={`${target},${converter}`} safe>
{target}
</option>
))}
</optgroup>
))}
</select>
</div>
</article>
<input
class={`
w-full btn-primary opacity-100
disabled:cursor-not-allowed disabled:opacity-50
`}
type="submit"
value="Convert"
disabled
/>
</form>
</main>
<script src="script.js" defer />
</>
</BaseHtml>
);
},
{
cookie: t.Cookie({
auth: t.Optional(t.String()),
jobId: t.Optional(t.String()),
}),
},
);

View File

@@ -1,39 +1,39 @@
import { Elysia, t } from "elysia";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { uploadsDir } from "../index";
import { userService } from "./user";
export const upload = new Elysia().use(userService).post(
"/upload",
async ({ body, redirect, user, cookie: { jobId } }) => {
if (!jobId?.value) {
return redirect(`${WEBROOT}/`, 302);
}
const existingJob = await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.get(jobId.value, user.id);
if (!existingJob) {
return redirect(`${WEBROOT}/`, 302);
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
if (body?.file) {
if (Array.isArray(body.file)) {
for (const file of body.file) {
await Bun.write(`${userUploadsDir}${file.name}`, file);
}
} else {
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
}
}
return {
message: "Files uploaded successfully.",
};
},
{ body: t.Object({ file: t.Files() }), auth: true },
);
import { Elysia, t } from "elysia";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { uploadsDir } from "../index";
import { userService } from "./user";
export const upload = new Elysia().use(userService).post(
"/upload",
async ({ body, redirect, user, cookie: { jobId } }) => {
if (!jobId?.value) {
return redirect(`${WEBROOT}/`, 302);
}
const existingJob = await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.get(jobId.value, user.id);
if (!existingJob) {
return redirect(`${WEBROOT}/`, 302);
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
if (body?.file) {
if (Array.isArray(body.file)) {
for (const file of body.file) {
await Bun.write(`${userUploadsDir}${file.name}`, file);
}
} else {
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
}
}
return {
message: "Files uploaded successfully.",
};
},
{ 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 {
/* Light mode */
--contrast: oklch(100% 0 0);
/* Neutral colors - Gray */
--neutral-950: oklch(98.5% 0.002 247.839);
--neutral-900: oklch(96.7% 0.003 264.542);
--neutral-800: oklch(92.8% 0.006 264.531);
--neutral-700: oklch(87.2% 0.01 258.338);
--neutral-600: oklch(70.7% 0.022 261.325);
--neutral-500: oklch(55.1% 0.027 264.364);
--neutral-400: oklch(44.6% 0.03 256.802);
--neutral-300: oklch(37.3% 0.034 259.733);
--neutral-200: oklch(26.9% 0 0);
--neutral-100: oklch(21% 0.034 264.665);
--neutral-50: oklch(13% 0.028 261.692);
/* lime-700 */
--accent-600: oklch(53.2% 0.157 131.589);
/* lime-600 */
--accent-500: oklch(64.8% 0.2 131.684);
/* lime-500 */
--accent-400: oklch(76.8% 0.233 130.85);
}
@media (prefers-color-scheme: dark) {
/* Dark mode */
:root {
--contrast: oklch(0% 0 0);
/* Neutral colors - Gray */
--neutral-950: oklch(13% 0.028 261.692);
--neutral-900: oklch(21% 0.034 264.665);
--neutral-800: oklch(27.8% 0.033 256.848);
--neutral-700: oklch(37.3% 0.034 259.733);
--neutral-600: oklch(44.6% 0.03 256.802);
--neutral-500: oklch(55.1% 0.027 264.364);
--neutral-400: oklch(70.7% 0.022 261.325);
--neutral-300: oklch(87.2% 0.01 258.338);
--neutral-200: oklch(92.8% 0.006 264.531);
--neutral-100: oklch(96.7% 0.003 264.542);
--neutral-50: oklch(98.5% 0.002 247.839);
/* lime-600 */
--accent-600: oklch(64.8% 0.2 131.684);
/* lime-500 */
--accent-500: oklch(76.8% 0.233 130.85);
/* lime-400 */
--accent-400: oklch(84.1% 0.238 128.85);
}
}
:root {
/* Light mode */
--contrast: oklch(100% 0 0);
/* Neutral colors - Gray */
--neutral-950: oklch(98.5% 0.002 247.839);
--neutral-900: oklch(96.7% 0.003 264.542);
--neutral-800: oklch(92.8% 0.006 264.531);
--neutral-700: oklch(87.2% 0.01 258.338);
--neutral-600: oklch(70.7% 0.022 261.325);
--neutral-500: oklch(55.1% 0.027 264.364);
--neutral-400: oklch(44.6% 0.03 256.802);
--neutral-300: oklch(37.3% 0.034 259.733);
--neutral-200: oklch(26.9% 0 0);
--neutral-100: oklch(21% 0.034 264.665);
--neutral-50: oklch(13% 0.028 261.692);
/* lime-700 */
--accent-600: oklch(53.2% 0.157 131.589);
/* lime-600 */
--accent-500: oklch(64.8% 0.2 131.684);
/* lime-500 */
--accent-400: oklch(76.8% 0.233 130.85);
}
@media (prefers-color-scheme: dark) {
/* Dark mode */
:root {
--contrast: oklch(0% 0 0);
/* Neutral colors - Gray */
--neutral-950: oklch(13% 0.028 261.692);
--neutral-900: oklch(21% 0.034 264.665);
--neutral-800: oklch(27.8% 0.033 256.848);
--neutral-700: oklch(37.3% 0.034 259.733);
--neutral-600: oklch(44.6% 0.03 256.802);
--neutral-500: oklch(55.1% 0.027 264.364);
--neutral-400: oklch(70.7% 0.022 261.325);
--neutral-300: oklch(87.2% 0.01 258.338);
--neutral-200: oklch(92.8% 0.006 264.531);
--neutral-100: oklch(96.7% 0.003 264.542);
--neutral-50: oklch(98.5% 0.002 247.839);
/* lime-600 */
--accent-600: oklch(64.8% 0.2 131.684);
/* lime-500 */
--accent-500: oklch(76.8% 0.233 130.85);
/* lime-400 */
--accent-400: oklch(84.1% 0.238 128.85);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,121 +1,121 @@
import type { ExecFileException } from "node:child_process";
import { expect } from "bun:test";
import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types";
export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(null, "Fake stdout", "");
};
const result = await convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(loggedMessage).toBe("stdout: Fake stdout");
}
export async function runConvertFailTest(convertFn: ConvertFnWithExecFile) {
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(new Error("Test error"), "", "");
};
expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile),
).rejects.toMatch(/error: Error: Test error/);
// Test with error object lacking 'message' property
const mockExecFileNoMessage: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
// Simulate a non-standard error object
callback({ notMessage: true } as unknown as ExecFileException, "", "");
};
expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileNoMessage),
).rejects.toMatch(/error:/i);
// Test with a non-object error (e.g., a string)
const mockExecFileStringError: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback("string error" as unknown as ExecFileException, "", "");
};
expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileStringError),
).rejects.toMatch(/error:/i);
}
export async function runConvertLogsStderror(convertFn: ConvertFnWithExecFile) {
const originalConsoleError = console.error;
let loggedMessage = "";
console.error = (msg) => {
loggedMessage = msg;
};
const mockExecFile = (
_cmd: string,
_args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void,
) => {
callback(null, "", "Fake stderr");
};
await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
console.error = originalConsoleError;
expect(loggedMessage).toBe("stderr: Fake stderr");
}
export async function runConvertLogsStderrorAndStdout(convertFn: ConvertFnWithExecFile) {
const originalConsoleError = console.error;
const originalConsoleLog = console.log;
let loggedError = "";
let loggedMessage = "";
console.error = (msg) => {
loggedError = msg;
};
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile = (
_cmd: string,
_args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void,
) => {
callback(null, "Fake stdout", "Fake stderr");
};
await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
console.error = originalConsoleError;
console.log = originalConsoleLog;
expect(loggedError).toBe("stderr: Fake stderr");
expect(loggedMessage).toBe("stdout: Fake stdout");
}
import { expect } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types";
export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(null, "Fake stdout", "");
};
const result = await convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(loggedMessage).toBe("stdout: Fake stdout");
}
export async function runConvertFailTest(convertFn: ConvertFnWithExecFile) {
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(new Error("Test error"), "", "");
};
expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile),
).rejects.toMatch(/error: Error: Test error/);
// Test with error object lacking 'message' property
const mockExecFileNoMessage: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
// Simulate a non-standard error object
callback({ notMessage: true } as unknown as ExecFileException, "", "");
};
expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileNoMessage),
).rejects.toMatch(/error:/i);
// Test with a non-object error (e.g., a string)
const mockExecFileStringError: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback("string error" as unknown as ExecFileException, "", "");
};
expect(
convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileStringError),
).rejects.toMatch(/error:/i);
}
export async function runConvertLogsStderror(convertFn: ConvertFnWithExecFile) {
const originalConsoleError = console.error;
let loggedMessage = "";
console.error = (msg) => {
loggedMessage = msg;
};
const mockExecFile = (
_cmd: string,
_args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void,
) => {
callback(null, "", "Fake stderr");
};
await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
console.error = originalConsoleError;
expect(loggedMessage).toBe("stderr: Fake stderr");
}
export async function runConvertLogsStderrorAndStdout(convertFn: ConvertFnWithExecFile) {
const originalConsoleError = console.error;
const originalConsoleLog = console.log;
let loggedError = "";
let loggedMessage = "";
console.error = (msg) => {
loggedError = msg;
};
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile = (
_cmd: string,
_args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void,
) => {
callback(null, "Fake stdout", "Fake stderr");
};
await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
console.error = originalConsoleError;
console.log = originalConsoleLog;
expect(loggedError).toBe("stderr: Fake stderr");
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 { convert } from "../../src/converters/imagemagick";
import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests";
let calls: string[][] = [];
beforeEach(() => {
calls = [];
});
runCommonTests(convert);
test("convert respects ico conversion target type", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
calls.push(_args);
callback(null, "Fake stdout", "");
};
const result = await convert("input.obj", "eps", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(calls[0]).toEqual(
expect.arrayContaining([
"-define",
"icon:auto-resize=256,128,64,48,32,16",
"-background",
"none",
"input.obj",
"output.ico",
]),
);
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert respects ico conversion target type with svg as input filetype", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
calls.push(_args);
callback(null, "Fake stdout", "");
};
const result = await convert("input.svg", "svg", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(calls[0]).toEqual(
expect.arrayContaining([
"-define",
"icon:auto-resize=256,128,64,48,32,16",
"-background",
"none",
"-density",
"512",
"input.svg",
"output.ico",
]),
);
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert respects ico conversion target type with emf as input filetype", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
calls.push(_args);
callback(null, "Fake stdout", "");
};
const result = await convert("input.emf", "emf", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(calls[0]).toEqual(
expect.arrayContaining([
"-define",
"icon:auto-resize=256,128,64,48,32,16",
"-background",
"none",
"emf:delegate=false",
"-density",
"300",
"white",
"-alpha",
"remove",
"input.emf",
"output.ico",
]),
);
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert respects emf as input filetype", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
calls.push(_args);
callback(null, "Fake stdout", "");
};
const result = await convert("input.emf", "emf", "obj", "output.obj", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(calls[0]).toEqual(
expect.arrayContaining([
"-define",
"emf:delegate=false",
"-density",
"300",
"-background",
"white",
"-alpha",
"remove",
"input.emf",
"output.obj",
]),
);
expect(loggedMessage).toBe("stdout: Fake stdout");
});
import { beforeEach, expect, test } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/imagemagick";
import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests";
let calls: string[][] = [];
beforeEach(() => {
calls = [];
});
runCommonTests(convert);
test("convert respects ico conversion target type", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
calls.push(_args);
callback(null, "Fake stdout", "");
};
const result = await convert("input.obj", "eps", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(calls[0]).toEqual(
expect.arrayContaining([
"-define",
"icon:auto-resize=256,128,64,48,32,16",
"-background",
"none",
"input.obj",
"output.ico",
]),
);
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert respects ico conversion target type with svg as input filetype", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
calls.push(_args);
callback(null, "Fake stdout", "");
};
const result = await convert("input.svg", "svg", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(calls[0]).toEqual(
expect.arrayContaining([
"-define",
"icon:auto-resize=256,128,64,48,32,16",
"-background",
"none",
"-density",
"512",
"input.svg",
"output.ico",
]),
);
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert respects ico conversion target type with emf as input filetype", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
calls.push(_args);
callback(null, "Fake stdout", "");
};
const result = await convert("input.emf", "emf", "ico", "output.ico", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(calls[0]).toEqual(
expect.arrayContaining([
"-define",
"icon:auto-resize=256,128,64,48,32,16",
"-background",
"none",
"emf:delegate=false",
"-density",
"300",
"white",
"-alpha",
"remove",
"input.emf",
"output.ico",
]),
);
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert respects emf as input filetype", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
calls.push(_args);
callback(null, "Fake stdout", "");
};
const result = await convert("input.emf", "emf", "obj", "output.obj", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(calls[0]).toEqual(
expect.arrayContaining([
"-define",
"emf:delegate=false",
"-density",
"300",
"-background",
"white",
"-alpha",
"remove",
"input.emf",
"output.obj",
]),
);
expect(loggedMessage).toBe("stdout: Fake stdout");
});

View File

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

View File

@@ -1,7 +1,7 @@
import { test } from "bun:test";
import { convert } from "../../src/converters/libheif";
import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert);
test.skip("dummy - required to trigger test detection", () => {});
import { test } from "bun:test";
import { convert } from "../../src/converters/libheif";
import { runCommonTests } from "./helpers/commonTests";
runCommonTests(convert);
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 { convert } from "../../src/converters/libjxl";
import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests";
let command: string = "";
beforeEach(() => {
command = "";
});
runCommonTests(convert);
test("convert uses djxl with input filetype being jxl", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
command = _cmd;
callback(null, "Fake stdout", "");
};
const result = await convert("input.jxl", "jxl", "png", "output.png", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(command).toEqual("djxl");
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert uses cjxl with output filetype being jxl", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
command = _cmd;
callback(null, "Fake stdout", "");
};
const result = await convert("input.png", "png", "jxl", "output.jxl", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(command).toEqual("cjxl");
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert uses empty string as command with neither input nor output filetype being jxl", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
command = _cmd;
callback(null, "Fake stdout", "");
};
const result = await convert("input.png", "png", "jpg", "output.jpg", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(command).toEqual("");
expect(loggedMessage).toBe("stdout: Fake stdout");
});
import { beforeEach, expect, test } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/libjxl";
import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests";
let command: string = "";
beforeEach(() => {
command = "";
});
runCommonTests(convert);
test("convert uses djxl with input filetype being jxl", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
command = _cmd;
callback(null, "Fake stdout", "");
};
const result = await convert("input.jxl", "jxl", "png", "output.png", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(command).toEqual("djxl");
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert uses cjxl with output filetype being jxl", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
command = _cmd;
callback(null, "Fake stdout", "");
};
const result = await convert("input.png", "png", "jxl", "output.jxl", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(command).toEqual("cjxl");
expect(loggedMessage).toBe("stdout: Fake stdout");
});
test("convert uses empty string as command with neither input nor output filetype being jxl", async () => {
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
command = _cmd;
callback(null, "Fake stdout", "");
};
const result = await convert("input.png", "png", "jpg", "output.jpg", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(result).toBe("Done");
expect(command).toEqual("");
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 { convert } from "../../src/converters/msgconvert";
import { ExecFileFn } from "../../src/converters/types";
test("convert rejects conversion if input filetype is not msg and output type is not eml", async () => {
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(null, "Fake stdout", "");
};
const expectedError = new Error(
"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(
expectedError,
);
});
test("convert rejects conversion on error", async () => {
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(new Error("Test error"), "", "");
};
const expectedError = new Error("msgconvert failed: Test error");
expect(convert("input.msg", "msg", "eml", "output.eml", undefined, mockExecFile)).rejects.toEqual(
expectedError,
);
});
test("convert logs stderr as warning", async () => {
const originalConsoleWarn = console.warn;
let loggedMessage = "";
console.warn = (msg) => {
loggedMessage = msg;
};
const mockExecFile = (
_cmd: string,
_args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void,
) => {
callback(null, "", "Fake stderr");
};
await convert("file.msg", "msg", "eml", "out.eml", undefined, mockExecFile);
console.error = originalConsoleWarn;
expect(loggedMessage).toBe("msgconvert stderr: Fake stderr");
});
import { expect, test } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/msgconvert";
import { ExecFileFn } from "../../src/converters/types";
test("convert rejects conversion if input filetype is not msg and output type is not eml", async () => {
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(null, "Fake stdout", "");
};
const expectedError = new Error(
"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(
expectedError,
);
});
test("convert rejects conversion on error", async () => {
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(new Error("Test error"), "", "");
};
const expectedError = new Error("msgconvert failed: Test error");
expect(convert("input.msg", "msg", "eml", "output.eml", undefined, mockExecFile)).rejects.toEqual(
expectedError,
);
});
test("convert logs stderr as warning", async () => {
const originalConsoleWarn = console.warn;
let loggedMessage = "";
console.warn = (msg) => {
loggedMessage = msg;
};
const mockExecFile = (
_cmd: string,
_args: string[],
callback: (err: Error | null, stdout: string, stderr: string) => void,
) => {
callback(null, "", "Fake stderr");
};
await convert("file.msg", "msg", "eml", "out.eml", undefined, mockExecFile);
console.error = originalConsoleWarn;
expect(loggedMessage).toBe("msgconvert stderr: Fake stderr");
});

View File

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

View File

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

View File

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

View File

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