From 17325a6e6d7774373362d4f71ac528045334af79 Mon Sep 17 00:00:00 2001 From: C4illin Date: Sun, 5 Oct 2025 14:19:13 +0000 Subject: [PATCH] chore: fix lint --- .devcontainer/Dockerfile | 17 - .devcontainer/devcontainer.json | 106 +- .github/FUNDING.yml | 30 +- .github/ISSUE_TEMPLATE/bug_report.md | 44 +- .github/ISSUE_TEMPLATE/converter_request.md | 52 +- .github/ISSUE_TEMPLATE/feature_request.md | 26 +- .github/workflows/check-lint.yml | 62 +- .github/workflows/docker-publish.yml | 346 ++--- .github/workflows/dockerhub-description.yml | 54 +- .github/workflows/release-please.yml | 50 +- .github/workflows/remove-docker-tag.yml | 42 +- .github/workflows/run-bun-test.yml | 62 +- .vscode/settings.json | 8 +- CHANGELOG.md | 466 +++--- README.md | 310 ++-- SECURITY.md | 18 +- biome.json | 144 +- bun.lock | 15 +- compose.yaml | 40 +- eslint.config.ts | 144 +- knip.json | 18 +- package.json | 5 +- postcss.config.js | 10 +- prettier.config.js | 26 +- public/results.js | 48 +- public/script.js | 502 +++--- public/site.webmanifest | 38 +- renovate.json | 18 +- reset.d.ts | 2 +- src/components/base.tsx | 88 +- src/components/header.tsx | 212 +-- src/converters/assimp.ts | 280 ++-- src/converters/calibre.ts | 172 +-- src/converters/dasel.ts | 96 +- src/converters/dvisvgm.ts | 98 +- src/converters/ffmpeg.ts | 1510 +++++++++---------- src/converters/graphicsmagick.ts | 674 ++++----- src/converters/imagemagick.ts | 984 ++++++------ src/converters/inkscape.ts | 112 +- src/converters/libheif.ts | 76 +- src/converters/libjxl.ts | 100 +- src/converters/libreoffice.ts | 354 ++--- src/converters/main.ts | 728 ++++----- src/converters/msgconvert.ts | 104 +- src/converters/pandoc.ts | 326 ++-- src/converters/potrace.ts | 100 +- src/converters/resvg.ts | 76 +- src/converters/types.ts | 34 +- src/converters/vips.ts | 278 ++-- src/converters/vtracer.ts | 160 +- src/converters/xelatex.ts | 90 +- src/db/db.ts | 82 +- src/db/types.ts | 46 +- src/helpers/env.ts | 50 +- src/helpers/normalizeFiletype.ts | 74 +- src/helpers/printVersions.ts | 372 ++--- src/helpers/tailwind.ts | 30 +- src/index.tsx | 188 +-- src/main.css | 64 +- src/pages/chooseConverter.tsx | 134 +- src/pages/convert.tsx | 188 +-- src/pages/deleteFile.tsx | 64 +- src/pages/download.tsx | 126 +- src/pages/history.tsx | 428 +++--- src/pages/listConverters.tsx | 148 +- src/pages/results.tsx | 422 +++--- src/pages/root.tsx | 498 +++--- src/pages/upload.tsx | 78 +- src/pages/user.tsx | 1041 ++++++------- src/theme/theme.css | 94 +- tests/converters/assimp.test.ts | 14 +- tests/converters/calibre.test.ts | 14 +- tests/converters/dvisvgm.test.ts | 182 +-- tests/converters/ffmpeg.test.ts | 362 ++--- tests/converters/graphicsmagick.test.ts | 14 +- tests/converters/helpers/commonTests.ts | 52 +- tests/converters/helpers/converters.ts | 242 +-- tests/converters/imagemagick.test.ts | 330 ++-- tests/converters/inkscape.test.ts | 14 +- tests/converters/libheif.test.ts | 14 +- tests/converters/libjxl.test.ts | 182 +-- tests/converters/msgconvert.test.ts | 122 +- tests/converters/potrace.test.ts | 14 +- tests/converters/resvg.test.ts | 14 +- tests/converters/vips.test.ts | 130 +- tests/converters/xelatex.test.ts | 14 +- tsconfig.json | 68 +- 87 files changed, 7624 insertions(+), 7640 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a817481..58f889f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2e2166e..7edd848 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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"] +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2870f64..de12f88 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -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'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index eebab81..d6ed108 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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` diff --git a/.github/ISSUE_TEMPLATE/converter_request.md b/.github/ISSUE_TEMPLATE/converter_request.md index bafd16b..7de554d 100644 --- a/.github/ISSUE_TEMPLATE/converter_request.md +++ b/.github/ISSUE_TEMPLATE/converter_request.md @@ -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?** - - - -**What converter should be added** - - - -**Are you willing to add it?** - - - -- [ ] Yes -- [ ] No - -**Additional context** - - +--- +name: Converter request +about: Suggest a converter for this project +title: "[Converter Request]" +labels: "converter request" +assignees: "" +--- + +**What file formats are missing?** + + + +**What converter should be added** + + + +**Are you willing to add it?** + + + +- [ ] Yes +- [ ] No + +**Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 66378f6..5140554 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -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. diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml index 20d74e0..875def5 100644 --- a/.github/workflows/check-lint.yml +++ b/.github/workflows/check-lint.yml @@ -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 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f09aa40..e95e739 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -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 }}' diff --git a/.github/workflows/dockerhub-description.yml b/.github/workflows/dockerhub-description.yml index 1cba485..966f1e8 100644 --- a/.github/workflows/dockerhub-description.yml +++ b/.github/workflows/dockerhub-description.yml @@ -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 diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 15e487e..1196083 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -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 diff --git a/.github/workflows/remove-docker-tag.yml b/.github/workflows/remove-docker-tag.yml index 90da8f1..ea8f3a9 100644 --- a/.github/workflows/remove-docker-tag.yml +++ b/.github/workflows/remove-docker-tag.yml @@ -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 }} diff --git a/.github/workflows/run-bun-test.yml b/.github/workflows/run-bun-test.yml index 5c3f520..3b5ae85 100644 --- a/.github/workflows/run-bun-test.yml +++ b/.github/workflows/run-bun-test.yml @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json index ea777da..fae8e3d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true -} +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f460e..49b69ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/README.md b/README.md index 2ef8b0d..cb31feb 100644 --- a/README.md +++ b/README.md @@ -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=) - -C4illin%2FConvertX | Trendshift - - - -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 | - - - -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=) - - - -### Tutorial - -> [!NOTE] -> These are written by other people, and may be outdated, incorrect or wrong. - -Tutorial in french: - -Tutorial in chinese: - -## 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 - - - Image with all contributors - - -## Star History - - - - - - Star History Chart - - +![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=) + +C4illin%2FConvertX | Trendshift + + + +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 | + + + +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=) + + + +### Tutorial + +> [!NOTE] +> These are written by other people, and may be outdated, incorrect or wrong. + +Tutorial in french: + +Tutorial in chinese: + +## 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 + + + Image with all contributors + + +## Star History + + + + + + Star History Chart + + diff --git a/SECURITY.md b/SECURITY.md index 49c9824..023c4bc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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. diff --git a/biome.json b/biome.json index 6d91002..b49e76e 100644 --- a/biome.json +++ b/biome.json @@ -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" + } + } +} diff --git a/bun.lock b/bun.lock index f37cb82..679619f 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/compose.yaml b/compose.yaml index 9fac41e..5b26ff8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -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 diff --git a/eslint.config.ts b/eslint.config.ts index ee5be63..7af097f 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -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, + }, + }, + }, +); diff --git a/knip.json b/knip.json index 2f7868b..bee52f8 100644 --- a/knip.json +++ b/knip.json @@ -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"] +} diff --git a/package.json b/package.json index 7927f58..7a8dc58 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/postcss.config.js b/postcss.config.js index e056d95..c2ddf74 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,5 +1,5 @@ -export default { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/prettier.config.js b/prettier.config.js index 2bd8151..df16566 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -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; diff --git a/public/results.js b/public/results.js index 8eef7cd..b125e93 100644 --- a/public/results.js +++ b/public/results.js @@ -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(); diff --git a/public/script.js b/public/script.js index 5ffc59c..8b89194 100644 --- a/public/script.js +++ b/public/script.js @@ -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 = ` - ${file.name} - - ${(file.size / 1024).toFixed(2)} kB - Remove - `; - - 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 = ` + ${file.name} + + ${(file.size / 1024).toFixed(2)} kB + Remove + `; + + 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(); diff --git a/public/site.webmanifest b/public/site.webmanifest index 23bce0f..09691b5 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -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" +} diff --git a/renovate.json b/renovate.json index 28d8064..ec32b06 100644 --- a/renovate.json +++ b/renovate.json @@ -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"] +} diff --git a/reset.d.ts b/reset.d.ts index 2f77a6a..a3d4a03 100644 --- a/reset.d.ts +++ b/reset.d.ts @@ -1 +1 @@ -import "@total-typescript/ts-reset"; +import "@total-typescript/ts-reset"; diff --git a/src/components/base.tsx b/src/components/base.tsx index 57bb910..9804813 100644 --- a/src/components/base.tsx +++ b/src/components/base.tsx @@ -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; -}) => ( - - - - - - {title} - - - - - - - - {children} - - - -); +import { Html } from "@elysiajs/html"; +import { version } from "../../package.json"; + +export const BaseHtml = ({ + children, + title = "ConvertX", + webroot = "", +}: { + children: JSX.Element; + title?: string; + webroot?: string; +}) => ( + + + + + + {title} + + + + + + + + {children} + + + +); diff --git a/src/components/header.tsx b/src/components/header.tsx index f5e5403..75fa627 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -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 = ( - - ); - } else { - rightNav = ( - - ); - } - - return ( -
- -
- ); -}; +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 = ( + + ); + } else { + rightNav = ( + + ); + } + + return ( +
+ +
+ ); +}; diff --git a/src/converters/assimp.ts b/src/converters/assimp.ts index 6dc003b..69f3258 100644 --- a/src/converters/assimp.ts +++ b/src/converters/assimp.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/calibre.ts b/src/converters/calibre.ts index 625a290..1d06695 100644 --- a/src/converters/calibre.ts +++ b/src/converters/calibre.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/dasel.ts b/src/converters/dasel.ts index 42d0ca6..741d325 100644 --- a/src/converters/dasel.ts +++ b/src/converters/dasel.ts @@ -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 { - 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 { + 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"); + } + }); + }); + }); +} diff --git a/src/converters/dvisvgm.ts b/src/converters/dvisvgm.ts index dc9ae52..a48bc69 100644 --- a/src/converters/dvisvgm.ts +++ b/src/converters/dvisvgm.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index 22da784..f7d183f 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -1,755 +1,755 @@ -import { execFile as execFileOriginal } from "node:child_process"; -import { ExecFileFn } from "./types"; - -// This could be done dynamically by running `ffmpeg -formats` and parsing the output -export const properties = { - from: { - muxer: [ - "264", - "265", - "266", - "302", - "3dostr", - "3g2", - "3gp", - "4xm", - "669", - "722", - "aa", - "aa3", - "aac", - "aax", - "ac3", - "ac4", - "ace", - "acm", - "act", - "adf", - "adp", - "ads", - "adx", - "aea", - "afc", - "aiff", - "aix", - "al", - "alaw", - "alias_pix", - "alp", - "alsa", - "amf", - "amr", - "amrnb", - "amrwb", - "ams", - "anm", - "ans", - "apc", - "ape", - "apl", - "apm", - "apng", - "aptx", - "aptxhd", - "aqt", - "aqtitle", - "argo_asf", - "argo_brp", - "art", - "asc", - "asf", - "asf_o", - "ass", - "ast", - "au", - "av1", - "avc", - "avi", - "avif", - "avr", - "avs", - "avs2", - "avs3", - "awb", - "bcstm", - "bethsoftvid", - "bfi", - "bfstm", - "bin", - "bink", - "binka", - "bit", - "bitpacked", - "bmv", - "bmp", - "bonk", - "boa", - "brender_pix", - "brstm", - "c2", - "c93", - "caf", - "cavsvideo", - "cdata", - "cdg", - "cdxl", - "cgi", - "cif", - "cine", - "codec2", - "codec2raw", - "concat", - "cri", - "dash", - "dat", - "data", - "daud", - "dav", - "dbm", - "dcstr", - "dds", - "derf", - "dfpwm", - "dfa", - "dhav", - "dif", - "digi", - "dirac", - "diz", - "dmf", - "dnxhd", - "dpx_pipe", - "dsf", - "dsicin", - "dsm", - "dss", - "dtk", - "dtm", - "dts", - "dtshd", - "dv", - "dvbsub", - "dvbtxt", - "dxa", - "ea", - "eac3", - "ea_cdata", - "epaf", - "exr_pipe", - "f32be", - "f32le", - "ec3", - "evc", - "f4v", - "f64be", - "f64le", - "fap", - "far", - "fbdev", - "ffmetadata", - "filmstrip", - "film_cpk", - "fits", - "flac", - "flic", - "flm", - "flv", - "frm", - "fsb", - "fwse", - "g722", - "g723_1", - "g726", - "g726le", - "g729", - "gdm", - "gdv", - "genh", - "gif", - "gsm", - "gxf", - "h261", - "h263", - "h264", - "h265", - "h266", - "h26l", - "hca", - "hcom", - "hevc", - "hls", - "hnm", - "ice", - "ico", - "idcin", - "idf", - "idx", - "iec61883", - "iff", - "ifv", - "ilbc", - "image2", - "imf", - "imx", - "ingenient", - "ipmovie", - "ipu", - "ircam", - "ism", - "isma", - "ismv", - "iss", - "it", - "iv8", - "ivf", - "ivr", - "j2b", - "j2k", - "jack", - "jacosub", - "jv", - "jpegls", - "jpeg", - "jxl", - "kmsgrab", - "kux", - "kvag", - "lavfi", - "laf", - "lmlm4", - "loas", - "lrc", - "luodat", - "lvf", - "lxf", - "m15", - "m2a", - "m4a", - "m4b", - "m4v", - "mac", - "mca", - "mcc", - "mdl", - "med", - "microdvd", - "mj2", - "mjpeg", - "mjpg", - "mk3d", - "mka", - "mks", - "mkv", - "mlp", - "mlv", - "mm", - "mmcmp", - "mmf", - "mms", - "mo3", - "mod", - "mods", - "moflex", - "mov", - "mp2", - "mp3", - "mp4", - "mpa", - "mpc", - "mpc8", - "mpeg", - "mpg", - "mpjpeg", - "mpl2", - "mpo", - "mpsub", - "mptm", - "msbc", - "msf", - "msnwctcp", - "msp", - "mt2", - "mtaf", - "mtm", - "mtv", - "mulaw", - "musx", - "mv", - "mvi", - "mxf", - "mxg", - "nc", - "nfo", - "nist", - "nistsphere", - "nsp", - "nst", - "nsv", - "nut", - "nuv", - "obu", - "ogg", - "okt", - "oma", - "omg", - "opus", - "openal", - "oss", - "osq", - "paf", - "pdv", - "pam", - "pbm", - "pcx", - "pgmyuv", - "pgm", - "pgx", - "photocd", - "pictor", - "pjs", - "plm", - "pmp", - "png", - "ppm", - "pp", - "psd", - "psm", - "psp", - "psxstr", - "pt36", - "ptm", - "pulse", - "pva", - "pvf", - "qcif", - "qcp", - "qdraw", - "r3d", - "rawvideo", - "rco", - "rcv", - "realtext", - "redspark", - "rgb", - "rl2", - "rm", - "roq", - "rpl", - "rka", - "rsd", - "rso", - "rt", - "rtp", - "rtsp", - "s16be", - "s16le", - "s24be", - "s24le", - "s32be", - "s32le", - "s337m", - "s3m", - "s8", - "sami", - "sap", - "sb", - "sbc", - "sbg", - "scc", - "sdns", - "sdp", - "sdr2", - "sds", - "sdx", - "ser", - "sf", - "sfx", - "sfx2", - "sga", - "sgi", - "shn", - "siff", - "sln", - "smi", - "smjpeg", - "smk", - "smush", - "sndio", - "sol", - "son", - "sox", - "spdif", - "sph", - "srt", - "ss2", - "ssa", - "st26", - "stk", - "stl", - "stm", - "stp", - "str", - "sub", - "sup", - "svag", - "svg", - "svs", - "sw", - "swf", - "tak", - "tco", - "tedcaptions", - "thd", - "thp", - "tiertexseq", - "tif", - "tiff", - "tmv", - "truehd", - "tta", - "tty", - "txd", - "txt", - "ty", - "ty+", - "u16be", - "u16le", - "u24be", - "u24le", - "u32be", - "u32le", - "u8", - "ub", - "ul", - "ult", - "umx", - "usm", - "uw", - "v", - "v210", - "v210x", - "vag", - "vb", - "vc1", - "vc1test", - "vidc", - "video4linux2", - "viv", - "vividas", - "vivo", - "vmd", - "vobsub", - "voc", - "vpk", - "vplayer", - "vqe", - "vqf", - "vql", - "vt", - "vtt", - "vvc", - "w64", - "wa", - "wav", - "way", - "wc3movie", - "webm", - "webp", - "webvtt", - "wow", - "wsaud", - "wsd", - "wsvqa", - "wtv", - "wv", - "wve", - "x11grab", - "xa", - "xbin", - "xl", - "xm", - "xmd", - "xmv", - "xpk", - "xvag", - "xwma", - "y4m", - "yop", - "yuv", - "yuv10", - ], - }, - to: { - muxer: [ - "264", - "265", - "266", - "302", - "3g2", - "3gp", - "a64", - "aac", - "ac3", - "ac4", - "adts", - "adx", - "afc", - "aif", - "aifc", - "aiff", - "al", - "amr", - "amv", - "apm", - "apng", - "aptx", - "aptxhd", - "asf", - "ass", - "ast", - "au", - "aud", - "av1.mkv", - "av1.mp4", - "avi", - "avif", - "avs", - "avs2", - "avs3", - "bit", - "bmp", - "c2", - "caf", - "cavs", - "chk", - "cpk", - "cvg", - "dfpwm", - "dnxhd", - "dnxhr", - "dpx", - "drc", - "dts", - "dv", - "dvd", - "eac3", - "ec3", - "evc", - "exr", - "f4v", - "ffmeta", - "fits", - "flac", - "flm", - "flv", - "g722", - "gif", - "gsm", - "gxf", - "h261", - "h263", - "h264.mkv", - "h264.mp4", - "h265.mkv", - "h265.mp4", - "h266.mkv", - "hdr", - "hevc", - "ico", - "im1", - "im24", - "im8", - "ircam", - "isma", - "ismv", - "ivf", - "j2c", - "j2k", - "jls", - "jp2", - "jpeg", - "jpg", - "js", - "jss", - "jxl", - "latm", - "lbc", - "ljpg", - "loas", - "lrc", - "m1v", - "m2a", - "m2t", - "m2ts", - "m2v", - "m3u8", - "m4a", - "m4b", - "m4v", - "mjpeg", - "mjpg", - "mkv", - "mlp", - "mmf", - "mov", - "mp2", - "mp3", - "mp4", - "mpa", - "mpd", - "mpeg", - "mpg", - "msbc", - "mts", - "mxf", - "nut", - "obu", - "oga", - "ogg", - "ogv", - "oma", - "opus", - "pam", - "pbm", - "pcm", - "pcx", - "pfm", - "pgm", - "pgmyuv", - "phm", - "pix", - "png", - "ppm", - "psp", - "qoi", - "ra", - "ras", - "rco", - "rcv", - "rgb", - "rm", - "roq", - "rs", - "rso", - "sb", - "sbc", - "scc", - "sf", - "sgi", - "sox", - "spdif", - "spx", - "srt", - "ssa", - "sub", - "sun", - "sunras", - "sup", - "sw", - "swf", - "tco", - "tga", - "thd", - "tif", - "tiff", - "ts", - "tta", - "ttml", - "tun", - "ub", - "ul", - "uw", - "vag", - "vbn", - "vc1", - "vc2", - "vob", - "voc", - "vtt", - "vvc", - "w64", - "wav", - "wbmp", - "webm", - "webp", - "wma", - "wmv", - "wtv", - "wv", - "xbm", - "xface", - "xml", - "xwd", - "y", - "y4m", - "yuv", - ], - }, -}; - -export async function convert( - filePath: string, - fileType: string, - convertTo: string, - targetPath: string, - options?: unknown, - execFile: ExecFileFn = execFileOriginal, // to make it mockable -): Promise { - let extraArgs: string[] = []; - let message = "Done"; - - if (convertTo === "ico") { - // make sure image is 256x256 or smaller - extraArgs = [ - "-filter:v", - "scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease", - ]; - message = "Done: resized to 256x256"; - } - - if (convertTo.split(".").length > 1) { - // support av1.mkv and av1.mp4 and h265.mp4 etc. - const split = convertTo.split("."); - const codec_short = split[0]; - - switch (codec_short) { - case "av1": - extraArgs.push("-c:v", "libaom-av1"); - break; - case "h264": - extraArgs.push("-c:v", "libx264"); - break; - case "h265": - extraArgs.push("-c:v", "libx265"); - break; - case "h266": - extraArgs.push("-c:v", "libx266"); - break; - } - } - - // Parse FFMPEG_ARGS environment variable into array - const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/) : []; - - return new Promise((resolve, reject) => { - execFile( - "ffmpeg", - [...ffmpegArgs, "-i", filePath, ...extraArgs, targetPath], - (error, stdout, stderr) => { - if (error) { - reject(`error: ${error}`); - } - - if (stdout) { - console.log(`stdout: ${stdout}`); - } - - if (stderr) { - console.error(`stderr: ${stderr}`); - } - - resolve(message); - }, - ); - }); -} +import { execFile as execFileOriginal } from "node:child_process"; +import { ExecFileFn } from "./types"; + +// This could be done dynamically by running `ffmpeg -formats` and parsing the output +export const properties = { + from: { + muxer: [ + "264", + "265", + "266", + "302", + "3dostr", + "3g2", + "3gp", + "4xm", + "669", + "722", + "aa", + "aa3", + "aac", + "aax", + "ac3", + "ac4", + "ace", + "acm", + "act", + "adf", + "adp", + "ads", + "adx", + "aea", + "afc", + "aiff", + "aix", + "al", + "alaw", + "alias_pix", + "alp", + "alsa", + "amf", + "amr", + "amrnb", + "amrwb", + "ams", + "anm", + "ans", + "apc", + "ape", + "apl", + "apm", + "apng", + "aptx", + "aptxhd", + "aqt", + "aqtitle", + "argo_asf", + "argo_brp", + "art", + "asc", + "asf", + "asf_o", + "ass", + "ast", + "au", + "av1", + "avc", + "avi", + "avif", + "avr", + "avs", + "avs2", + "avs3", + "awb", + "bcstm", + "bethsoftvid", + "bfi", + "bfstm", + "bin", + "bink", + "binka", + "bit", + "bitpacked", + "bmv", + "bmp", + "bonk", + "boa", + "brender_pix", + "brstm", + "c2", + "c93", + "caf", + "cavsvideo", + "cdata", + "cdg", + "cdxl", + "cgi", + "cif", + "cine", + "codec2", + "codec2raw", + "concat", + "cri", + "dash", + "dat", + "data", + "daud", + "dav", + "dbm", + "dcstr", + "dds", + "derf", + "dfpwm", + "dfa", + "dhav", + "dif", + "digi", + "dirac", + "diz", + "dmf", + "dnxhd", + "dpx_pipe", + "dsf", + "dsicin", + "dsm", + "dss", + "dtk", + "dtm", + "dts", + "dtshd", + "dv", + "dvbsub", + "dvbtxt", + "dxa", + "ea", + "eac3", + "ea_cdata", + "epaf", + "exr_pipe", + "f32be", + "f32le", + "ec3", + "evc", + "f4v", + "f64be", + "f64le", + "fap", + "far", + "fbdev", + "ffmetadata", + "filmstrip", + "film_cpk", + "fits", + "flac", + "flic", + "flm", + "flv", + "frm", + "fsb", + "fwse", + "g722", + "g723_1", + "g726", + "g726le", + "g729", + "gdm", + "gdv", + "genh", + "gif", + "gsm", + "gxf", + "h261", + "h263", + "h264", + "h265", + "h266", + "h26l", + "hca", + "hcom", + "hevc", + "hls", + "hnm", + "ice", + "ico", + "idcin", + "idf", + "idx", + "iec61883", + "iff", + "ifv", + "ilbc", + "image2", + "imf", + "imx", + "ingenient", + "ipmovie", + "ipu", + "ircam", + "ism", + "isma", + "ismv", + "iss", + "it", + "iv8", + "ivf", + "ivr", + "j2b", + "j2k", + "jack", + "jacosub", + "jv", + "jpegls", + "jpeg", + "jxl", + "kmsgrab", + "kux", + "kvag", + "lavfi", + "laf", + "lmlm4", + "loas", + "lrc", + "luodat", + "lvf", + "lxf", + "m15", + "m2a", + "m4a", + "m4b", + "m4v", + "mac", + "mca", + "mcc", + "mdl", + "med", + "microdvd", + "mj2", + "mjpeg", + "mjpg", + "mk3d", + "mka", + "mks", + "mkv", + "mlp", + "mlv", + "mm", + "mmcmp", + "mmf", + "mms", + "mo3", + "mod", + "mods", + "moflex", + "mov", + "mp2", + "mp3", + "mp4", + "mpa", + "mpc", + "mpc8", + "mpeg", + "mpg", + "mpjpeg", + "mpl2", + "mpo", + "mpsub", + "mptm", + "msbc", + "msf", + "msnwctcp", + "msp", + "mt2", + "mtaf", + "mtm", + "mtv", + "mulaw", + "musx", + "mv", + "mvi", + "mxf", + "mxg", + "nc", + "nfo", + "nist", + "nistsphere", + "nsp", + "nst", + "nsv", + "nut", + "nuv", + "obu", + "ogg", + "okt", + "oma", + "omg", + "opus", + "openal", + "oss", + "osq", + "paf", + "pdv", + "pam", + "pbm", + "pcx", + "pgmyuv", + "pgm", + "pgx", + "photocd", + "pictor", + "pjs", + "plm", + "pmp", + "png", + "ppm", + "pp", + "psd", + "psm", + "psp", + "psxstr", + "pt36", + "ptm", + "pulse", + "pva", + "pvf", + "qcif", + "qcp", + "qdraw", + "r3d", + "rawvideo", + "rco", + "rcv", + "realtext", + "redspark", + "rgb", + "rl2", + "rm", + "roq", + "rpl", + "rka", + "rsd", + "rso", + "rt", + "rtp", + "rtsp", + "s16be", + "s16le", + "s24be", + "s24le", + "s32be", + "s32le", + "s337m", + "s3m", + "s8", + "sami", + "sap", + "sb", + "sbc", + "sbg", + "scc", + "sdns", + "sdp", + "sdr2", + "sds", + "sdx", + "ser", + "sf", + "sfx", + "sfx2", + "sga", + "sgi", + "shn", + "siff", + "sln", + "smi", + "smjpeg", + "smk", + "smush", + "sndio", + "sol", + "son", + "sox", + "spdif", + "sph", + "srt", + "ss2", + "ssa", + "st26", + "stk", + "stl", + "stm", + "stp", + "str", + "sub", + "sup", + "svag", + "svg", + "svs", + "sw", + "swf", + "tak", + "tco", + "tedcaptions", + "thd", + "thp", + "tiertexseq", + "tif", + "tiff", + "tmv", + "truehd", + "tta", + "tty", + "txd", + "txt", + "ty", + "ty+", + "u16be", + "u16le", + "u24be", + "u24le", + "u32be", + "u32le", + "u8", + "ub", + "ul", + "ult", + "umx", + "usm", + "uw", + "v", + "v210", + "v210x", + "vag", + "vb", + "vc1", + "vc1test", + "vidc", + "video4linux2", + "viv", + "vividas", + "vivo", + "vmd", + "vobsub", + "voc", + "vpk", + "vplayer", + "vqe", + "vqf", + "vql", + "vt", + "vtt", + "vvc", + "w64", + "wa", + "wav", + "way", + "wc3movie", + "webm", + "webp", + "webvtt", + "wow", + "wsaud", + "wsd", + "wsvqa", + "wtv", + "wv", + "wve", + "x11grab", + "xa", + "xbin", + "xl", + "xm", + "xmd", + "xmv", + "xpk", + "xvag", + "xwma", + "y4m", + "yop", + "yuv", + "yuv10", + ], + }, + to: { + muxer: [ + "264", + "265", + "266", + "302", + "3g2", + "3gp", + "a64", + "aac", + "ac3", + "ac4", + "adts", + "adx", + "afc", + "aif", + "aifc", + "aiff", + "al", + "amr", + "amv", + "apm", + "apng", + "aptx", + "aptxhd", + "asf", + "ass", + "ast", + "au", + "aud", + "av1.mkv", + "av1.mp4", + "avi", + "avif", + "avs", + "avs2", + "avs3", + "bit", + "bmp", + "c2", + "caf", + "cavs", + "chk", + "cpk", + "cvg", + "dfpwm", + "dnxhd", + "dnxhr", + "dpx", + "drc", + "dts", + "dv", + "dvd", + "eac3", + "ec3", + "evc", + "exr", + "f4v", + "ffmeta", + "fits", + "flac", + "flm", + "flv", + "g722", + "gif", + "gsm", + "gxf", + "h261", + "h263", + "h264.mkv", + "h264.mp4", + "h265.mkv", + "h265.mp4", + "h266.mkv", + "hdr", + "hevc", + "ico", + "im1", + "im24", + "im8", + "ircam", + "isma", + "ismv", + "ivf", + "j2c", + "j2k", + "jls", + "jp2", + "jpeg", + "jpg", + "js", + "jss", + "jxl", + "latm", + "lbc", + "ljpg", + "loas", + "lrc", + "m1v", + "m2a", + "m2t", + "m2ts", + "m2v", + "m3u8", + "m4a", + "m4b", + "m4v", + "mjpeg", + "mjpg", + "mkv", + "mlp", + "mmf", + "mov", + "mp2", + "mp3", + "mp4", + "mpa", + "mpd", + "mpeg", + "mpg", + "msbc", + "mts", + "mxf", + "nut", + "obu", + "oga", + "ogg", + "ogv", + "oma", + "opus", + "pam", + "pbm", + "pcm", + "pcx", + "pfm", + "pgm", + "pgmyuv", + "phm", + "pix", + "png", + "ppm", + "psp", + "qoi", + "ra", + "ras", + "rco", + "rcv", + "rgb", + "rm", + "roq", + "rs", + "rso", + "sb", + "sbc", + "scc", + "sf", + "sgi", + "sox", + "spdif", + "spx", + "srt", + "ssa", + "sub", + "sun", + "sunras", + "sup", + "sw", + "swf", + "tco", + "tga", + "thd", + "tif", + "tiff", + "ts", + "tta", + "ttml", + "tun", + "ub", + "ul", + "uw", + "vag", + "vbn", + "vc1", + "vc2", + "vob", + "voc", + "vtt", + "vvc", + "w64", + "wav", + "wbmp", + "webm", + "webp", + "wma", + "wmv", + "wtv", + "wv", + "xbm", + "xface", + "xml", + "xwd", + "y", + "y4m", + "yuv", + ], + }, +}; + +export async function convert( + filePath: string, + fileType: string, + convertTo: string, + targetPath: string, + options?: unknown, + execFile: ExecFileFn = execFileOriginal, // to make it mockable +): Promise { + let extraArgs: string[] = []; + let message = "Done"; + + if (convertTo === "ico") { + // make sure image is 256x256 or smaller + extraArgs = [ + "-filter:v", + "scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease", + ]; + message = "Done: resized to 256x256"; + } + + if (convertTo.split(".").length > 1) { + // support av1.mkv and av1.mp4 and h265.mp4 etc. + const split = convertTo.split("."); + const codec_short = split[0]; + + switch (codec_short) { + case "av1": + extraArgs.push("-c:v", "libaom-av1"); + break; + case "h264": + extraArgs.push("-c:v", "libx264"); + break; + case "h265": + extraArgs.push("-c:v", "libx265"); + break; + case "h266": + extraArgs.push("-c:v", "libx266"); + break; + } + } + + // Parse FFMPEG_ARGS environment variable into array + const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/) : []; + + return new Promise((resolve, reject) => { + execFile( + "ffmpeg", + [...ffmpegArgs, "-i", filePath, ...extraArgs, targetPath], + (error, stdout, stderr) => { + if (error) { + reject(`error: ${error}`); + } + + if (stdout) { + console.log(`stdout: ${stdout}`); + } + + if (stderr) { + console.error(`stderr: ${stderr}`); + } + + resolve(message); + }, + ); + }); +} diff --git a/src/converters/graphicsmagick.ts b/src/converters/graphicsmagick.ts index cc9d938..4af168e 100644 --- a/src/converters/graphicsmagick.ts +++ b/src/converters/graphicsmagick.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/imagemagick.ts b/src/converters/imagemagick.ts index f3286b0..69eb359 100644 --- a/src/converters/imagemagick.ts +++ b/src/converters/imagemagick.ts @@ -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 { - 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 { + 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"); + }, + ); + }); +} diff --git a/src/converters/inkscape.ts b/src/converters/inkscape.ts index 9e8170f..dce123b 100644 --- a/src/converters/inkscape.ts +++ b/src/converters/inkscape.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/libheif.ts b/src/converters/libheif.ts index 0b5ff46..3df1f52 100644 --- a/src/converters/libheif.ts +++ b/src/converters/libheif.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/libjxl.ts b/src/converters/libjxl.ts index 5e8e57e..5abe9ba 100644 --- a/src/converters/libjxl.ts +++ b/src/converters/libjxl.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/libreoffice.ts b/src/converters/libreoffice.ts index a161238..02f389a 100644 --- a/src/converters/libreoffice.ts +++ b/src/converters/libreoffice.ts @@ -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> = { - 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 { - 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> = { + 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 { + 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"); + }); + }); +} diff --git a/src/converters/main.ts b/src/converters/main.ts index 9b2f1e2..738574d 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -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; - to: Record; - 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(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, -) { - 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[] = []; - 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> = {}; - -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 => { - 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 = {}; - -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 = {}; -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; + to: Record; + 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(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, +) { + 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[] = []; + 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> = {}; + +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 => { + 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 = {}; + +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 = {}; +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}`); diff --git a/src/converters/msgconvert.ts b/src/converters/msgconvert.ts index 7b360fe..612a2ef 100644 --- a/src/converters/msgconvert.ts +++ b/src/converters/msgconvert.ts @@ -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 { - 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 { + 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.`, + ), + ); + } + }); +} diff --git a/src/converters/pandoc.ts b/src/converters/pandoc.ts index 3a54f14..c14c98b 100644 --- a/src/converters/pandoc.ts +++ b/src/converters/pandoc.ts @@ -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 { - // 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 { + // 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"); + }); + }); +} diff --git a/src/converters/potrace.ts b/src/converters/potrace.ts index cfb30c6..b18f202 100644 --- a/src/converters/potrace.ts +++ b/src/converters/potrace.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/resvg.ts b/src/converters/resvg.ts index 22894aa..fcdc2e4 100644 --- a/src/converters/resvg.ts +++ b/src/converters/resvg.ts @@ -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 { - 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 { + 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"); + }); + }); +} diff --git a/src/converters/types.ts b/src/converters/types.ts index dde0035..1bfe36c 100644 --- a/src/converters/types.ts +++ b/src/converters/types.ts @@ -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; +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; diff --git a/src/converters/vips.ts b/src/converters/vips.ts index c2ed331..310053f 100644 --- a/src/converters/vips.ts +++ b/src/converters/vips.ts @@ -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 { - // 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 { + // 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"); + }); + }); +} diff --git a/src/converters/vtracer.ts b/src/converters/vtracer.ts index 0930db9..31ce110 100644 --- a/src/converters/vtracer.ts +++ b/src/converters/vtracer.ts @@ -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 { - 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 = [ - "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 { + 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 = [ + "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"); + }); + }); +} diff --git a/src/converters/xelatex.ts b/src/converters/xelatex.ts index a4fd5bb..6bd0802 100644 --- a/src/converters/xelatex.ts +++ b/src/converters/xelatex.ts @@ -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 { - 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 { + 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"); + }, + ); + }); +} diff --git a/src/db/db.ts b/src/db/db.ts index 3081768..1193d5d 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -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; diff --git a/src/db/types.ts b/src/db/types.ts index 13d5500..4825711 100644 --- a/src/db/types.ts +++ b/src/db/types.ts @@ -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; +} diff --git a/src/helpers/env.ts b/src/helpers/env.ts index ae52c01..4c8c067 100644 --- a/src/helpers/env.ts +++ b/src/helpers/env.ts @@ -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; diff --git a/src/helpers/normalizeFiletype.ts b/src/helpers/normalizeFiletype.ts index f6aba73..cf9389a 100644 --- a/src/helpers/normalizeFiletype.ts +++ b/src/helpers/normalizeFiletype.ts @@ -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; + } +}; diff --git a/src/helpers/printVersions.ts b/src/helpers/printVersions.ts index 37b1874..bd94729 100644 --- a/src/helpers/printVersions.ts +++ b/src/helpers/printVersions.ts @@ -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]}`); + } + }); +} diff --git a/src/helpers/tailwind.ts b/src/helpers/tailwind.ts index 0e26d75..a823171 100644 --- a/src/helpers/tailwind.ts +++ b/src/helpers/tailwind.ts @@ -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; +}; diff --git a/src/index.tsx b/src/index.tsx index 52e8674..b95b4b8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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(); +} diff --git a/src/main.css b/src/main.css index 44d0a30..72ded0e 100644 --- a/src/main.css +++ b/src/main.css @@ -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; +} diff --git a/src/pages/chooseConverter.tsx b/src/pages/chooseConverter.tsx index 310fd67..8e7a3f8 100644 --- a/src/pages/chooseConverter.tsx +++ b/src/pages/chooseConverter.tsx @@ -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 ( - <> - - - - - ); - }, - { 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 ( + <> + + + + + ); + }, + { body: t.Object({ fileType: t.String() }) }, +); diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index e4469f6..6ae9825 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -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, + }, +); diff --git a/src/pages/deleteFile.tsx b/src/pages/deleteFile.tsx index 37f2779..9599bec 100644 --- a/src/pages/deleteFile.tsx +++ b/src/pages/deleteFile.tsx @@ -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 }, +); diff --git a/src/pages/download.tsx b/src/pages/download.tsx index 2729dc9..6b7c3c1 100644 --- a/src/pages/download.tsx +++ b/src/pages/download.tsx @@ -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, + }, + ); diff --git a/src/pages/history.tsx b/src/pages/history.tsx index 083c4ad..82ce6f6 100644 --- a/src/pages/history.tsx +++ b/src/pages/history.tsx @@ -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 ( - - <> -
-
-
-

Results

- - - - - - - - - - - - - {userJobs.map((job) => ( - <> - - - - - - - - - - - - - ))} - -
- Expand details - - Time - - Files - - Files Done - - Status - - View -
- - - - {new Date(job.date_created).toLocaleTimeString(LANGUAGE)}{job.num_files}{job.finished_files}{job.status} - - View - -
-
-
- - - - ); - }, { - 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 ( + + <> +
+
+
+

Results

+ + + + + + + + + + + + + {userJobs.map((job) => ( + <> + + + + + + + + + + + + + ))} + +
+ Expand details + + Time + + Files + + Files Done + + Status + + View +
+ + + + {new Date(job.date_created).toLocaleTimeString(LANGUAGE)}{job.num_files}{job.finished_files}{job.status} + + View + +
+
+
+ + + + ); + }, + { + auth: true, + }, +); diff --git a/src/pages/listConverters.tsx b/src/pages/listConverters.tsx index d2789f4..1950b46 100644 --- a/src/pages/listConverters.tsx +++ b/src/pages/listConverters.tsx @@ -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 ( - - <> -
-
-
-

Converters

- - - - - - - - - - {Object.entries(getAllTargets()).map(([converter, targets]) => { - const inputs = getAllInputs(converter); - return ( - - - - - - ); - })} - -
ConverterFrom (Count)To (Count)
{converter} - Count: {inputs.length} -
    - {inputs.map((input) => ( -
  • {input}
  • - ))} -
-
- Count: {targets.length} -
    - {targets.map((target) => ( -
  • {target}
  • - ))} -
-
-
-
- - - ); - }, { - 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 ( + + <> +
+
+
+

Converters

+ + + + + + + + + + {Object.entries(getAllTargets()).map(([converter, targets]) => { + const inputs = getAllInputs(converter); + return ( + + + + + + ); + })} + +
ConverterFrom (Count)To (Count)
{converter} + Count: {inputs.length} +
    + {inputs.map((input) => ( +
  • {input}
  • + ))} +
+
+ Count: {targets.length} +
    + {targets.map((target) => ( +
  • {target}
  • + ))} +
+
+
+
+ + + ); + }, + { + auth: true, + }, +); diff --git a/src/pages/results.tsx b/src/pages/results.tsx index 4e472f7..a85cb3a 100644 --- a/src/pages/results.tsx +++ b/src/pages/results.tsx @@ -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 ( - - ); -} - -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 ( - - <> -
-
- -
-