1 Commits

Author SHA1 Message Date
C4illin
a8f2cd4e9e feat: poppler working for some formats 2024-05-30 12:06:02 +02:00
38 changed files with 810 additions and 1356 deletions

View File

@@ -1,7 +0,0 @@
version = 1
[[analyzers]]
name = "javascript"
[analyzers.meta]
environment = ["nodejs"]

55
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,55 @@
/** @type {import("eslint").Linter.Config} */
const config = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["isaacscript", "import"],
extends: [
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:prettier/recommended",
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
tsconfigRootDir: __dirname,
project: [
"./tsconfig.json",
"./cli/tsconfig.eslint.json", // separate eslint config for the CLI since we want to lint and typecheck differently due to template files
"./upgrade/tsconfig.json",
"./www/tsconfig.json",
],
},
overrides: [
// Template files don't have reliable type information
{
files: ["./cli/template/**/*.{ts,tsx}"],
extends: ["plugin:@typescript-eslint/disable-type-checked"],
},
],
rules: {
// These off/not-configured-the-way-we-want lint rules we like & opt into
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_" },
],
"@typescript-eslint/consistent-type-imports": [
"error",
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
],
"import/consistent-type-specifier-style": ["error", "prefer-inline"],
// For educational purposes we format our comments/jsdoc nicely
"isaacscript/complete-sentences-jsdoc": "warn",
"isaacscript/format-jsdoc-comments": "warn",
// These lint rules don't make sense for us but are enabled in the preset configs
"@typescript-eslint/no-confusing-void-expression": "off",
"@typescript-eslint/restrict-template-expressions": "off",
// This rule doesn't seem to be working properly
"@typescript-eslint/prefer-nullish-coalescing": "off",
},
};
module.exports = config;

23
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for npm
- package-ecosystem: "npm" # See documentation for possible values
versioning-strategy: increase
directory: "/" # Location of package manifests
schedule:
interval: "daily"
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
# Maintain dependencies for Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"

28
.github/workflows/bun-dependabot.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: 'Dependabot: Update bun.lockb'
on:
pull_request:
paths:
- "package.json"
permissions:
contents: write
jobs:
update-bun-lockb:
name: "Update bun.lockb"
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- uses: oven-sh/setup-bun@v1
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
- run: |
bun install
git add bun.lockb
git config --global user.name 'dependabot[bot]'
git config --global user.email 'dependabot[bot]@users.noreply.github.com'
git commit --amend --no-edit
git push --force

View File

@@ -58,10 +58,9 @@ jobs:
# https://github.com/docker/build-push-action # https://github.com/docker/build-push-action
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

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

View File

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

1
.gitignore vendored
View File

@@ -47,4 +47,3 @@ package-lock.json
/db /db
/data /data
/Bruno /Bruno
/tsconfig.tsbuildinfo

View File

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

View File

@@ -1,101 +0,0 @@
# Changelog
## [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))

View File

@@ -1,63 +0,0 @@
FROM oven/bun:1-debian as base
WORKDIR /app
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# FROM base AS install-libjxl-tools
# download
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
# FROM base AS prerelease
# COPY --from=install /temp/dev/node_modules node_modules
# COPY . .
# # [optional] tests & build
# ENV NODE_ENV=production
# RUN bun test
# RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
LABEL maintainer="Emrik Östling (C4illin)"
LABEL description="ConvertX: self-hosted online file converter supporting 700+ file formats."
LABEL repo="https://github.com/C4illin/ConvertX"
# install additional dependencies
RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
&& apt-get install -y \
pandoc \
texlive-latex-recommended \
texlive-fonts-recommended \
texlive-latex-extra \
ffmpeg \
graphicsmagick \
ghostscript \
libvips-tools
# # libjxl is not available in the official debian repositories
# RUN wget https://github.com/libjxl/libjxl/releases/download/v0.10.2/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -O /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz \
# && mkdir -p /tmp/libjxl \
# && tar -xvf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -C /tmp/libjxl \
# && dpkg -i /tmp/libjxl/libjxl_0.10.2_amd64.deb /tmp/libjxl/jxl_0.10.2_amd64.deb \
# && rm -rf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz /tmp/libjxl
COPY --from=install /temp/prod/node_modules node_modules
# COPY --from=prerelease /app/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json .
COPY . .
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]

View File

@@ -1,5 +1,4 @@
FROM oven/bun:1.1.29-alpine AS base FROM oven/bun:1-debian as base
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
WORKDIR /app WORKDIR /app
# install dependencies into temp directory # install dependencies into temp directory
@@ -14,12 +13,6 @@ RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/ COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production RUN cd /temp/prod && bun install --frozen-lockfile --production
FROM base AS builder
RUN apk --no-cache add curl gcc
ENV PATH=/root/.cargo/bin:$PATH
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
RUN cargo install resvg
# copy node_modules from temp directory # copy node_modules from temp directory
# then copy all (non-ignored) project files into the image # then copy all (non-ignored) project files into the image
# FROM base AS prerelease # FROM base AS prerelease
@@ -38,28 +31,21 @@ LABEL description="ConvertX: self-hosted online file converter supporting 700+ f
LABEL repo="https://github.com/C4illin/ConvertX" LABEL repo="https://github.com/C4illin/ConvertX"
# install additional dependencies # install additional dependencies
RUN apk --no-cache add \ RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
&& apt-get install -y \
pandoc \ pandoc \
texlive \ texlive-latex-recommended \
texlive-xetex \ texlive-fonts-recommended \
texmf-dist-latexextra \ texlive-latex-extra \
ffmpeg \ ffmpeg \
graphicsmagick \ graphicsmagick \
ghostscript \ ghostscript \
vips-tools \ libvips-tools
vips-poppler \
vips-jxl \
libjxl-tools
# this might be needed for some latex use cases, will add it if needed.
# texmf-dist-fontsextra \
COPY --from=install /temp/prod/node_modules node_modules COPY --from=install /temp/prod/node_modules node_modules
COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
# COPY --from=prerelease /app/src/index.tsx /app/src/ # COPY --from=prerelease /app/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json . # COPY --from=prerelease /app/package.json .
COPY . . COPY . .
EXPOSE 3000/tcp EXPOSE 3000/tcp
ENV NODE_ENV=production
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ] ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]

View File

@@ -1,13 +1,8 @@
![ConvertX](images/logo.png) ![ConvertX](images/logo.png)
# ConvertX # ConvertX
[![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml) [![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
[![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=)
![GitHub top language](https://img.shields.io/github/languages/top/C4illin/ConvertX)
A self-hosted online file converter. Supports 831 different formats. Written with TypeScript, Bun and Elysia. A self-hosted online file converter. Supports 831 different formats. Written with Typescript, Bun and Elysia.
## Features ## Features
@@ -18,45 +13,37 @@ A self-hosted online file converter. Supports 831 different formats. Written wit
## Converters supported ## Converters supported
| Converter | Use case | Converts from | Converts to | | Converter | Use case | Converts from | Converts to |
|------------------------------------------------------------------------------|---------------|---------------|-------------| |----------------|---------------|---------------|-------------|
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 | | Vips | Images (fast) | 45 | 23 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 | | PDFLaTeX | Documents | 1 | 1 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 | | Pandoc | Documents | 43 | 65 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 | | GraphicsMagick | Images | 166 | 133 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 | | FFmpeg | Video | ~473 | ~280 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 |
<!-- many ffmpeg fileformats are duplicates --> <!-- many ffmpeg fileformats are duplicates -->
Any missing converter? Open an issue or pull request!
## Deployment ## Deployment
```yml ```yml
# docker-compose.yml # docker-compose.yml
services: services:
convertx: convertx:
image: ghcr.io/c4illin/convertx image: ghcr.io/c4illin/convertx:main
container_name: convertx
restart: unless-stopped
ports: ports:
- "3000:3000" - "3000:3000"
environment: # Defaults are listed below. All are optional. environment: # Defaults are listed below. All are optional.
- ACCOUNT_REGISTRATION=false # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account) - ACCOUNT_REGISTRATION=false # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally - HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
- AUTO_DELETE_EVERY_N_HOURS=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
volumes: volumes:
- convertx:/app/data - convertx:/app/data
``` ```
or <!-- or
```bash ```bash
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx docker run ghcr.io/c4illin/convertx:master -p 3000:3000 -e ACCOUNT_REGISTRATION=false -v /path/you/want:/app/data
``` ``` -->
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account. Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
@@ -66,31 +53,13 @@ If you get unable to open database file run `chown -R $USER:$USER path` on the p
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/ Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
## 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 below and open issues for the list of todos.
## Todo ## Todo
- [x] Add messages for errors in converters - [x] Add messages for errors in converters
- [x] Add searchable list of formats
- [ ] Add options for converters - [ ] Add options for converters
- [ ] Add more converters
- [ ] Divide index.tsx into smaller components - [ ] Divide index.tsx into smaller components
- [ ] Add tests - [ ] Add tests
- [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible. - [ ] Add searchable list of formats
- [ ] Make errors logs visible from the web ui
- [ ] Add more converters:
- [ ] [deark](https://github.com/jsummers/deark)
- [ ] LibreOffice
- [ ] [dvisvgm](https://github.com/mgieseki/dvisvgm)
## Contributors ## Contributors

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"formatWithErrors": true, "formatWithErrors": true,
@@ -9,9 +9,6 @@
"lineWidth": 80, "lineWidth": 80,
"attributePosition": "auto" "attributePosition": "auto"
}, },
"files": {
"ignore": ["**/node_modules/**", "**/pico.lime.min.css"]
},
"organizeImports": { "enabled": true }, "organizeImports": { "enabled": true },
"linter": { "linter": {
"enabled": true, "enabled": true,

BIN
bun.lockb

Binary file not shown.

View File

@@ -2,12 +2,10 @@ services:
convertx: convertx:
build: build:
context: . context: .
# dockerfile: Debian.Dockerfile
volumes: volumes:
- ./data:/app/data - ./data:/app/data
environment: environment:
- ACCOUNT_REGISTRATION=true - ACCOUNT_REGISTRATION=true
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234
- ALLOW_UNAUTHENTICATED=true
ports: ports:
- 3000:3000 - 3000:3000

View File

@@ -1,36 +0,0 @@
import { fixupPluginRules } from "@eslint/compat";
import tseslint from "typescript-eslint";
import eslint from "@eslint/js";
import deprecationPlugin from "eslint-plugin-deprecation";
import eslintCommentsPlugin from "eslint-plugin-eslint-comments";
import importPlugin from "eslint-plugin-import";
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
export default tseslint.config(
{
plugins: {
"@typescript-eslint": tseslint.plugin,
deprecation: fixupPluginRules(deprecationPlugin),
"eslint-comments": eslintCommentsPlugin,
import: fixupPluginRules(importPlugin),
"simple-import-sort": simpleImportSortPlugin,
},
},
{
ignores: ["**/node_modules/**", "**/public/**"],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
ecmaVersion: "latest",
sourceType: "module",
project: ["./tsconfig.json"],
},
},
},
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,57 +1,40 @@
{ {
"name": "convertx-frontend", "name": "convertx-frontend",
"version": "0.5.0", "version": "1.0.50",
"scripts": { "scripts": {
"dev": "bun run --watch src/index.tsx", "dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx", "hot": "bun run --hot src/index.tsx",
"format": "biome format --write ./src", "format": "biome format --write ./src",
"css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat", "css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat"
"lint": "run-p 'lint:*'",
"lint:tsc": "tsc --noEmit",
"lint:knip": "knip",
"lint:biome": "biome lint --error-on-warnings ./src"
}, },
"dependencies": { "dependencies": {
"@elysiajs/cookie": "^0.8.0", "@elysiajs/cookie": "^0.8.0",
"@elysiajs/html": "1.0.2", "@elysiajs/html": "^1.0.2",
"@elysiajs/jwt": "^1.1.1", "@elysiajs/jwt": "^1.0.2",
"@elysiajs/static": "1.0.3", "@elysiajs/static": "^1.0.3",
"elysia": "^1.1.12" "elysia": "^1.0.22",
"node-poppler": "^7.2.0"
}, },
"module": "src/index.tsx", "module": "src/index.tsx",
"type": "module",
"bun-create": { "bun-create": {
"start": "bun run src/index.tsx" "start": "bun run src/index.tsx"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.2", "@biomejs/biome": "1.7.3",
"@eslint/compat": "^1.1.1", "@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"@eslint/js": "^9.9.1", "@kitajs/ts-html-plugin": "^4.0.1",
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@kitajs/ts-html-plugin": "^4.0.2",
"@picocss/pico": "^2.0.6", "@picocss/pico": "^2.0.6",
"@total-typescript/ts-reset": "^0.6.1", "@total-typescript/ts-reset": "^0.5.1",
"@types/bun": "^1.1.8", "@types/bun": "^1.1.3",
"@types/eslint": "^9.6.1", "@types/eslint": "^8.56.10",
"@types/node": "^22.5.4", "@types/node": "^20.12.13",
"@typescript-eslint/eslint-plugin": "^8.4.0", "@types/ws": "^8.5.10",
"@typescript-eslint/parser": "^8.4.0", "@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"cpy-cli": "^5.0.0", "cpy-cli": "^5.0.0",
"eslint": "^9.9.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-eslint-comments": "^3.2.0", "prettier": "^3.2.5",
"eslint-plugin-import": "^2.30.0", "typescript": "^5.4.5"
"eslint-plugin-isaacscript": "^4.0.0", }
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"knip": "^5.29.2",
"npm-run-all2": "^6.2.2",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"typescript-eslint": "^8.4.0"
},
"trustedDependencies": [
"@biomejs/biome"
]
} }

View File

@@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

View File

@@ -1,7 +1,4 @@
export const BaseHtml = ({ export const BaseHtml = ({ children, title = "ConvertX" }) => (
children,
title = "ConvertX",
}: { children: JSX.Element; title?: string }) => (
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View File

@@ -30,7 +30,7 @@ export const Header = ({
} }
return ( return (
<header class="container"> <header className="container">
<nav> <nav>
<ul> <ul>
<li> <li>

View File

@@ -260,7 +260,6 @@ export const properties = {
"mpegts", "mpegts",
"mpegtsraw", "mpegtsraw",
"mpegvideo", "mpegvideo",
"mpg",
"mpjpeg", "mpjpeg",
"mpl2", "mpl2",
"mpo", "mpo",

View File

@@ -1,71 +0,0 @@
import { exec } from "node:child_process";
// declare possible conversions
export const properties = {
from: {
jxl: ["jxl"],
images: [
"apng",
"exr",
"gif",
"jpeg",
"pam",
"pfm",
"pgm",
"pgx",
"png",
"ppm",
],
},
to: {
jxl: [
"apng",
"exr",
"gif",
"jpeg",
"pam",
"pfm",
"pgm",
"pgx",
"png",
"ppm",
],
images: ["jxl"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
): Promise<string> {
let tool = "";
if (fileType === "jxl") {
tool = "djxl";
}
if (convertTo === "jxl") {
tool = "cjxl";
}
return new Promise((resolve, reject) => {
exec(`${tool} "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("success");
});
});
}

View File

@@ -16,19 +16,11 @@ import {
} from "./graphicsmagick"; } from "./graphicsmagick";
import { import {
convert as convertxelatex, convert as convertPdflatex,
properties as propertiesxelatex, properties as propertiesPdflatex,
} from "./xelatex"; } from "./pdflatex";
import { import { convert as convertPoppler, properties as propertiesPoppler } from "./poppler";
convert as convertLibjxl,
properties as propertiesLibjxl,
} from "./libjxl";
import {
convert as convertresvg,
properties as propertiesresvg,
} from "./resvg";
import { normalizeFiletype } from "../helpers/normalizeFiletype"; import { normalizeFiletype } from "../helpers/normalizeFiletype";
@@ -60,21 +52,17 @@ const properties: {
) => any; ) => any;
}; };
} = { } = {
libjxl: {
properties: propertiesLibjxl,
converter: convertLibjxl,
},
resvg: {
properties: propertiesresvg,
converter: convertresvg,
},
vips: { vips: {
properties: propertiesImage, properties: propertiesImage,
converter: convertImage, converter: convertImage,
}, },
xelatex: { pdflatex: {
properties: propertiesxelatex, properties: propertiesPdflatex,
converter: convertxelatex, converter: convertPdflatex,
},
poppler: {
properties: propertiesPoppler,
converter: convertPoppler,
}, },
pandoc: { pandoc: {
properties: propertiesPandoc, properties: propertiesPandoc,
@@ -120,8 +108,9 @@ export async function mainConverter(
for (const key in converterObj.properties.from) { for (const key in converterObj.properties.from) {
if ( if (
converterObj?.properties?.from[key]?.includes(fileType) && // HOW??
converterObj?.properties?.to[key]?.includes(convertTo) converterObj.properties.from[key].includes(fileType) &&
converterObj.properties.to[key].includes(convertTo)
) { ) {
converterFunc = converterObj.converter; converterFunc = converterObj.converter;
break; break;
@@ -210,7 +199,7 @@ for (const converterName in properties) {
} }
possibleInputs.sort(); possibleInputs.sort();
const getPossibleInputs = () => { export const getPossibleInputs = () => {
return possibleInputs; return possibleInputs;
}; };
@@ -225,9 +214,9 @@ for (const converterName in properties) {
for (const key in converterProperties.to) { for (const key in converterProperties.to) {
if (allTargets[converterName]) { if (allTargets[converterName]) {
allTargets[converterName].push(...(converterProperties.to[key] || [])); allTargets[converterName].push(...converterProperties.to[key]);
} else { } else {
allTargets[converterName] = converterProperties.to[key] || []; allTargets[converterName] = converterProperties.to[key];
} }
} }
} }
@@ -246,9 +235,9 @@ for (const converterName in properties) {
for (const key in converterProperties.from) { for (const key in converterProperties.from) {
if (allInputs[converterName]) { if (allInputs[converterName]) {
allInputs[converterName].push(...(converterProperties.from[key] || [])); allInputs[converterName].push(...converterProperties.from[key]);
} else { } else {
allInputs[converterName] = converterProperties.from[key] || []; allInputs[converterName] = converterProperties.from[key];
} }
} }
} }

119
src/converters/old.sharp.ts Normal file
View File

@@ -0,0 +1,119 @@
import sharp from "sharp";
import type { FormatEnum } from "sharp";
// declare possible conversions
export const properties = {
from: {
images: [
"avif",
"bif",
"csv",
"exr",
"fits",
"gif",
"hdr.gz",
"hdr",
"heic",
"heif",
"img.gz",
"img",
"j2c",
"j2k",
"jp2",
"jpeg",
"jpx",
"jxl",
"mat",
"mrxs",
"ndpi",
"nia.gz",
"nia",
"nii.gz",
"nii",
"pdf",
"pfm",
"pgm",
"pic",
"png",
"ppm",
"raw",
"scn",
"svg",
"svs",
"svslide",
"szi",
"tif",
"tiff",
"v",
"vips",
"vms",
"vmu",
"webp",
"zip",
],
},
to: {
images: [
"avif",
"dzi",
"fits",
"gif",
"hdr.gz",
"heic",
"heif",
"img.gz",
"j2c",
"j2k",
"jp2",
"jpeg",
"jpx",
"jxl",
"mat",
"nia.gz",
"nia",
"nii.gz",
"nii",
"png",
"tiff",
"vips",
"webp",
],
},
options: {
svg: {
scale: {
description: "Scale the image up or down",
type: "number",
default: 1,
},
},
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: keyof FormatEnum,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
) {
if (fileType === "svg") {
const scale = options.scale || 1;
const metadata = await sharp(filePath).metadata();
if (!metadata || !metadata.width || !metadata.height) {
throw new Error("Could not get metadata from image");
}
const newWidth = Math.round(metadata.width * scale);
const newHeight = Math.round(metadata.height * scale);
return await sharp(filePath)
.resize(newWidth, newHeight)
.toFormat(convertTo)
.toFile(targetPath);
}
return await sharp(filePath).toFormat(convertTo).toFile(targetPath);
}

View File

@@ -127,15 +127,9 @@ export function convert(
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any, options?: any,
): Promise<string> { ): Promise<string> {
// set xelatex here
const xelatex = ["pdf", "latex"];
let option = "";
if (xelatex.includes(convertTo)) {
option = "--pdf-engine=xelatex";
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec( exec(
`pandoc ${option} "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`, `pandoc "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
(error, stdout, stderr) => { (error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);

View File

@@ -19,13 +19,9 @@ export function convert(
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "") // const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
const outputPath = targetPath const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "")
.split("/")
.slice(0, -1)
.join("/")
.replace("./", "");
exec( exec(
`latexmk -xelatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`, `pdflatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
(error, stdout, stderr) => { (error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);

115
src/converters/poppler.ts Normal file
View File

@@ -0,0 +1,115 @@
const { Poppler } = require("node-poppler");
const poppler = new Poppler();
export const properties = {
from: {
text: ["pdf"],
},
to: {
text: [
"jpeg",
"png",
"tiff",
"eps",
"icc",
"pdf",
"svg",
"ps",
"html",
"text",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
): Promise<string> {
return new Promise((resolve, reject) => {
const cairoFiles = [
"jpeg",
"png",
"tiff",
"eps",
"icc",
"pdf",
"svg",
"ps",
];
if (cairoFiles.includes(convertTo)) {
const popplerOptions: {
jpegFile?: boolean;
pngFile?: boolean;
tiffFile?: boolean;
epsFile?: boolean;
iccFile?: boolean;
pdfFile?: boolean;
svgFile?: boolean;
psFile?: boolean;
} = {};
switch (convertTo) {
case "jpeg":
popplerOptions.jpegFile = true;
break;
case "png":
popplerOptions.pngFile = true;
break;
case "tiff":
popplerOptions.tiffFile = true;
break;
case "eps":
popplerOptions.epsFile = true;
break;
case "icc":
popplerOptions.iccFile = true;
break;
case "pdf":
popplerOptions.pdfFile = true;
break;
case "svg":
popplerOptions.svgFile = true;
break;
case "ps":
popplerOptions.psFile = true;
break;
default:
reject(`Invalid convertTo option: ${convertTo}`);
}
poppler
.pdfToCairo(filePath, targetPath, popplerOptions)
.then(() => {
resolve("success");
})
.catch((err: Error) => {
reject(err);
});
} else if (convertTo === "html") {
poppler
.pdfToHtml(filePath, targetPath)
.then(() => {
resolve("success");
})
.catch((err: Error) => {
reject(err);
});
} else if (convertTo === "text") {
poppler
.pdfToText(filePath, targetPath)
.then(() => {
resolve("success");
})
.catch((err: Error) => {
reject(err);
});
} else {
reject(`Invalid convertTo option: ${convertTo}`);
}
});
}

View File

@@ -1,37 +0,0 @@
import { exec } from "node:child_process";
export const properties = {
from: {
images: ["svg"],
},
to: {
images: ["png"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
): Promise<string> {
return new Promise((resolve, reject) => {
exec(`resvg "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("success");
});
});
}

View File

@@ -113,15 +113,9 @@ export function convert(
// .toFormat(convertTo) // .toFormat(convertTo)
// .toFile(targetPath); // .toFile(targetPath);
// } // }
let action = "copy";
if (fileType === "pdf") {
action = "pdfload";
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec( exec(`vips copy "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
`vips ${action} "${filePath}" "${targetPath}"`,
(error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);
} }
@@ -135,7 +129,6 @@ export function convert(
} }
resolve("success"); resolve("success");
}, });
);
}); });
} }

View File

@@ -25,6 +25,8 @@ export const normalizeOutputFiletype = (filetype: string): string => {
return "tex"; return "tex";
case "markdown": case "markdown":
return "md"; return "md";
case "text":
return "txt";
default: default:
return lowercaseFiletype; return lowercaseFiletype;
} }

View File

@@ -1,95 +0,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("gm version", (error, stdout) => {
if (error) {
console.error("GraphicsMagick is not installed.");
}
if (stdout) {
console.log(stdout.split("\n")[0]);
}
});
exec("djxl --version", (error, stdout) => {
if (error) {
console.error("libjxl-tools is not installed.");
}
if (stdout) {
console.log(stdout.split("\n")[0]);
}
});
exec("xelatex -version", (error, stdout) => {
if (error) {
console.error("Tex Live with XeTeX is not installed.");
}
if (stdout) {
console.log(stdout.split("\n")[0]);
}
});
exec("resvg -V", (error, stdout) => {
if (error) {
console.error("resvg is not installed");
}
if (stdout) {
console.log(`resvg v${stdout.split("\n")[0]}`);
}
});
exec("bun -v", (error, stdout) => {
if (error) {
console.error("Bun is not installed. wait what");
}
if (stdout) {
console.log(`Bun v${stdout.split("\n")[0]}`);
}
});
}

View File

@@ -1,12 +1,12 @@
import cookie from "@elysiajs/cookie";
import { html } from "@elysiajs/html";
import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt";
import { staticPlugin } from "@elysiajs/static";
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { Elysia, t } from "elysia"; import { randomUUID } from "node:crypto";
import { randomInt, randomUUID } from "node:crypto";
import { rmSync } from "node:fs"; import { rmSync } from "node:fs";
import { mkdir, unlink } from "node:fs/promises"; import { mkdir, unlink } from "node:fs/promises";
import cookie from "@elysiajs/cookie";
import { html } from "@elysiajs/html";
import { jwt } from "@elysiajs/jwt";
import { staticPlugin } from "@elysiajs/static";
import { Elysia, t } from "elysia";
import { BaseHtml } from "./components/base"; import { BaseHtml } from "./components/base";
import { Header } from "./components/header"; import { Header } from "./components/header";
import { import {
@@ -19,23 +19,15 @@ import {
normalizeFiletype, normalizeFiletype,
normalizeOutputFiletype, normalizeOutputFiletype,
} from "./helpers/normalizeFiletype"; } from "./helpers/normalizeFiletype";
import "./helpers/printVersions";
mkdir("./data", { recursive: true }).catch(console.error);
const db = new Database("./data/mydb.sqlite", { create: true }); const db = new Database("./data/mydb.sqlite", { create: true });
const uploadsDir = "./data/uploads/"; const uploadsDir = "./data/uploads/";
const outputDir = "./data/output/"; const outputDir = "./data/output/";
const ACCOUNT_REGISTRATION = const ACCOUNT_REGISTRATION =
process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false; process.env.ACCOUNT_REGISTRATION === "true" || false;
const HTTP_ALLOWED = const HTTP_ALLOWED = process.env.HTTP_ALLOWED === "true" || false;
process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
const ALLOW_UNAUTHENTICATED =
process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
: 24;
// fileNames: fileNames, // fileNames: fileNames,
// filesToConvert: fileNames.length, // filesToConvert: fileNames.length,
@@ -82,37 +74,33 @@ if (dbVersion === 0) {
let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false; let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
class User { interface IUser {
id!: number; id: number;
email!: string; email: string;
password!: string; password: string;
} }
class Filename { interface IFileNames {
id!: number; id: number;
job_id!: number; job_id: number;
file_name!: string; file_name: string;
output_file_name!: string; output_file_name: string;
status!: string; status: string;
} }
class Jobs { interface IJobs {
finished_files!: number; finished_files: number;
id!: number; id: number;
user_id!: number; user_id: number;
date_created!: string; date_created: string;
status!: string; status: string;
num_files!: number; num_files: number;
} }
// enable WAL mode // enable WAL mode
db.exec("PRAGMA journal_mode = WAL;"); db.exec("PRAGMA journal_mode = WAL;");
const app = new Elysia({ const app = new Elysia()
serve: {
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
},
})
.use(cookie()) .use(cookie())
.use(html()) .use(html())
.use( .use(
@@ -121,7 +109,7 @@ const app = new Elysia({
schema: t.Object({ schema: t.Object({
id: t.String(), id: t.String(),
}), }),
secret: process.env.JWT_SECRET ?? randomUUID(), secret: process.env.JWT_SECRET || randomUUID(),
exp: "7d", exp: "7d",
}), }),
) )
@@ -133,7 +121,7 @@ const app = new Elysia({
) )
.get("/setup", ({ redirect }) => { .get("/setup", ({ redirect }) => {
if (!FIRST_RUN) { if (!FIRST_RUN) {
return redirect("/login", 302); return redirect("/login");
} }
return ( return (
@@ -176,12 +164,11 @@ const app = new Elysia({
}) })
.get("/register", ({ redirect }) => { .get("/register", ({ redirect }) => {
if (!ACCOUNT_REGISTRATION) { if (!ACCOUNT_REGISTRATION) {
return redirect("/login", 302); return redirect("/login");
} }
return ( return (
<BaseHtml title="ConvertX | Register"> <BaseHtml title="ConvertX | Register">
<>
<Header accountRegistration={ACCOUNT_REGISTRATION} /> <Header accountRegistration={ACCOUNT_REGISTRATION} />
<main class="container"> <main class="container">
<article> <article>
@@ -212,7 +199,6 @@ const app = new Elysia({
</form> </form>
</article> </article>
</main> </main>
</>
</BaseHtml> </BaseHtml>
); );
}) })
@@ -220,7 +206,7 @@ const app = new Elysia({
"/register", "/register",
async ({ body, set, redirect, jwt, cookie: { auth } }) => { async ({ body, set, redirect, jwt, cookie: { auth } }) => {
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) { if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
return redirect("/login", 302); return redirect("/login");
} }
if (FIRST_RUN) { if (FIRST_RUN) {
@@ -243,17 +229,9 @@ const app = new Elysia({
savedPassword, savedPassword,
); );
const user = db const user = (await db
.query("SELECT * FROM users WHERE email = ?") .query("SELECT * FROM users WHERE email = ?")
.as(User) .get(body.email)) as IUser;
.get(body.email);
if (!user) {
set.status = 500;
return {
message: "Failed to create user.",
};
}
const accessToken = await jwt.sign({ const accessToken = await jwt.sign({
id: String(user.id), id: String(user.id),
@@ -275,13 +253,13 @@ const app = new Elysia({
sameSite: "strict", sameSite: "strict",
}); });
return redirect("/", 302); return redirect("/");
}, },
{ body: t.Object({ email: t.String(), password: t.String() }) }, { body: t.Object({ email: t.String(), password: t.String() }) },
) )
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => { .get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
if (FIRST_RUN) { if (FIRST_RUN) {
return redirect("/setup", 302); return redirect("/setup");
} }
// if already logged in, redirect to home // if already logged in, redirect to home
@@ -289,7 +267,7 @@ const app = new Elysia({
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (user) { if (user) {
return redirect("/", 302); return redirect("/");
} }
auth.remove(); auth.remove();
@@ -297,7 +275,6 @@ const app = new Elysia({
return ( return (
<BaseHtml title="ConvertX | Login"> <BaseHtml title="ConvertX | Login">
<>
<Header accountRegistration={ACCOUNT_REGISTRATION} /> <Header accountRegistration={ACCOUNT_REGISTRATION} />
<main class="container"> <main class="container">
<article> <article>
@@ -335,17 +312,15 @@ const app = new Elysia({
</form> </form>
</article> </article>
</main> </main>
</>
</BaseHtml> </BaseHtml>
); );
}) })
.post( .post(
"/login", "/login",
async function handler({ body, set, redirect, jwt, cookie: { auth } }) { async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
const existingUser = db const existingUser = (await db
.query("SELECT * FROM users WHERE email = ?") .query("SELECT * FROM users WHERE email = ?")
.as(User) .get(body.email)) as IUser;
.get(body.email);
if (!existingUser) { if (!existingUser) {
set.status = 403; set.status = 403;
@@ -386,7 +361,7 @@ const app = new Elysia({
sameSite: "strict", sameSite: "strict",
}); });
return redirect("/", 302); return redirect("/");
}, },
{ body: t.Object({ email: t.String(), password: t.String() }) }, { body: t.Object({ email: t.String(), password: t.String() }) },
) )
@@ -395,70 +370,39 @@ const app = new Elysia({
auth.remove(); auth.remove();
} }
return redirect("/login", 302); return redirect("/login");
}) })
.post("/logoff", ({ redirect, cookie: { auth } }) => { .post("/logoff", ({ redirect, cookie: { auth } }) => {
if (auth?.value) { if (auth?.value) {
auth.remove(); auth.remove();
} }
return redirect("/login", 302); return redirect("/login");
}) })
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
if (FIRST_RUN) { if (FIRST_RUN) {
return redirect("/setup", 302); return redirect("/setup");
} }
if (!auth?.value && !ALLOW_UNAUTHENTICATED) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
// validate jwt // validate jwt
let user: ({ id: string } & JWTPayloadSpec) | false = false; const user = await jwt.verify(auth.value);
if (auth?.value) { if (!user) {
user = await jwt.verify(auth.value); return redirect("/login");
}
if (user !== false && user.id) {
if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
// make sure user exists in db // make sure user exists in db
const existingUser = db const existingUser = (await db
.query("SELECT * FROM users WHERE id = ?") .query("SELECT * FROM users WHERE id = ?")
.as(User) .get(user.id)) as IUser;
.get(user.id);
if (!existingUser) { if (!existingUser) {
if (auth?.value) { if (auth?.value) {
auth.remove(); auth.remove();
} }
return redirect("/login", 302); return redirect("/login");
}
}
}
} else if (ALLOW_UNAUTHENTICATED) {
const newUserId = String(randomInt(2 ** 24, Number.MAX_SAFE_INTEGER));
const accessToken = await jwt.sign({
id: newUserId,
});
user = { id: newUserId };
if (!auth) {
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
};
}
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 60 * 60 * 24 * 1,
sameSite: "strict",
});
}
if (!user) {
return redirect("/login", 302);
} }
// create a new job // create a new job
@@ -489,7 +433,6 @@ const app = new Elysia({
return ( return (
<BaseHtml> <BaseHtml>
<>
<Header loggedIn /> <Header loggedIn />
<main class="container"> <main class="container">
<article> <article>
@@ -509,92 +452,14 @@ const app = new Elysia({
))} ))}
</select> */} </select> */}
</article> </article>
<form <form method="post" action="/convert">
method="post"
action="/convert"
style={{ position: "relative" }}>
<input type="hidden" name="file_names" id="file_names" /> <input type="hidden" name="file_names" id="file_names" />
<article> <article>
<input <select name="convert_to" aria-label="Convert to" required>
type="search"
name="convert_to_search"
placeholder="Search for conversions"
autocomplete="off"
/>
<div class="select_container">
<article
class="convert_to_popup"
hidden
style={{
flexDirection: "column",
display: "flex",
zIndex: 2,
position: "absolute",
maxHeight: "50vh",
width: "90vw",
overflowY: "scroll",
margin: "0px",
overflowX: "hidden",
}}>
{Object.entries(getAllTargets()).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<article
class="convert_to_group"
data-converter={converter}
style={{
borderColor: "gray",
padding: "2px",
}}
>
<header
style={{ fontSize: "20px", fontWeight: "bold" }}
safe>
{converter}
</header>
<ul
class="convert_to_target"
style={{
display: "flex",
flexDirection: "row",
gap: "5px",
flexWrap: "wrap",
}}>
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class="target"
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
style={{ fontSize: "15px", padding: "5px" }}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
),
)}
</article>
{/* Hidden element which determines the format to convert the file too and the converter to use */}
<select
name="convert_to"
aria-label="Convert to"
required
hidden>
<option selected disabled value=""> <option selected disabled value="">
Convert to Convert to
</option> </option>
{Object.entries(getAllTargets()).map( {Object.entries(getAllTargets()).map(([converter, targets]) => (
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation> // biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<optgroup label={converter}> <optgroup label={converter}>
{targets.map((target) => ( {targets.map((target) => (
@@ -604,16 +469,13 @@ const app = new Elysia({
</option> </option>
))} ))}
</optgroup> </optgroup>
), ))}
)}
</select> </select>
</div>
</article> </article>
<input type="submit" value="Convert" /> <input type="submit" value="Convert" />
</form> </form>
</main> </main>
<script src="script.js" defer /> <script src="script.js" defer />
</>
</BaseHtml> </BaseHtml>
); );
}) })
@@ -621,67 +483,7 @@ const app = new Elysia({
"/conversions", "/conversions",
({ body }) => { ({ body }) => {
return ( return (
<> <select name="convert_to" aria-label="Convert to" required>
<article
class="convert_to_popup"
hidden
style={{
flexDirection: "column",
display: "flex",
zIndex: 2,
position: "absolute",
maxHeight: "50vh",
width: "90vw",
overflowY: "scroll",
margin: "0px",
overflowX: "hidden",
}}>
{Object.entries(getPossibleTargets(body.fileType)).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<article
class="convert_to_group"
data-converter={converter}
style={{
borderColor: "gray",
padding: "2px",
}}
>
<header style={{ fontSize: "20px", fontWeight: "bold" }} safe>
{converter}
</header>
<ul
class="convert_to_target"
style={{
display: "flex",
flexDirection: "row",
gap: "5px",
flexWrap: "wrap",
}}>
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class="target"
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
style={{ fontSize: "15px", padding: "5px" }}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
),
)}
</article>
<select name="convert_to" aria-label="Convert to" required hidden>
<option selected disabled value=""> <option selected disabled value="">
Convert to Convert to
</option> </option>
@@ -699,7 +501,6 @@ const app = new Elysia({
), ),
)} )}
</select> </select>
</>
); );
}, },
{ body: t.Object({ fileType: t.String() }) }, { body: t.Object({ fileType: t.String() }) },
@@ -708,16 +509,16 @@ const app = new Elysia({
"/upload", "/upload",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
if (!jobId?.value) { if (!jobId?.value) {
return redirect("/", 302); return redirect("/");
} }
const existingJob = await db const existingJob = await db
@@ -725,7 +526,7 @@ const app = new Elysia({
.get(jobId.value, user.id); .get(jobId.value, user.id);
if (!existingJob) { if (!existingJob) {
return redirect("/", 302); return redirect("/");
} }
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
@@ -736,8 +537,13 @@ const app = new Elysia({
await Bun.write(`${userUploadsDir}${file.name}`, file); await Bun.write(`${userUploadsDir}${file.name}`, file);
} }
} else { } else {
// biome-ignore lint/complexity/useLiteralKeys: weird error await Bun.write(
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file); `${userUploadsDir}${
// biome-ignore lint/complexity/useLiteralKeys: ts bug
body.file["name"]
}`,
body.file,
);
} }
} }
@@ -751,16 +557,16 @@ const app = new Elysia({
"/delete", "/delete",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
if (!jobId?.value) { if (!jobId?.value) {
return redirect("/", 302); return redirect("/");
} }
const existingJob = await db const existingJob = await db
@@ -768,7 +574,7 @@ const app = new Elysia({
.get(jobId.value, user.id); .get(jobId.value, user.id);
if (!existingJob) { if (!existingJob) {
return redirect("/", 302); return redirect("/");
} }
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
@@ -781,25 +587,24 @@ const app = new Elysia({
"/convert", "/convert",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
if (!jobId?.value) { if (!jobId?.value) {
return redirect("/", 302); return redirect("/");
} }
const existingJob = db const existingJob = (await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?") .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.as(Jobs) .get(jobId.value, user.id)) as IJobs;
.get(jobId.value, user.id);
if (!existingJob) { if (!existingJob) {
return redirect("/", 302); return redirect("/");
} }
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
@@ -815,33 +620,34 @@ const app = new Elysia({
); );
} }
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? ""); const convertTo = normalizeFiletype(
body.convert_to.split(",")[0] as string,
);
const converterName = body.convert_to.split(",")[1]; const converterName = body.convert_to.split(",")[1];
const fileNames = JSON.parse(body.file_names) as string[]; const fileNames = JSON.parse(body.file_names) as string[];
if (!Array.isArray(fileNames) || fileNames.length === 0) { if (!Array.isArray(fileNames) || fileNames.length === 0) {
return redirect("/", 302); return redirect("/");
} }
db.query( db.run(
"UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2", "UPDATE jobs SET num_files = ?, status = 'pending' WHERE id = ?",
).run(fileNames.length, jobId.value); fileNames.length,
jobId.value,
);
const query = db.query( const query = db.query(
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)", "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?, ?, ?, ?)",
); );
// Start the conversion process in the background // Start the conversion process in the background
Promise.all( Promise.all(
fileNames.map(async (fileName) => { fileNames.map(async (fileName) => {
const filePath = `${userUploadsDir}${fileName}`; const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop() ?? ""; const fileTypeOrig = fileName.split(".").pop() as string;
const fileType = normalizeFiletype(fileTypeOrig); const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo); const newFileExt = normalizeOutputFiletype(convertTo);
const newFileName = fileName.replace( const newFileName = fileName.replace(fileTypeOrig, newFileExt);
new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
newFileExt,
);
const targetPath = `${userOutputDir}${newFileName}`; const targetPath = `${userOutputDir}${newFileName}`;
const result = await mainConverter( const result = await mainConverter(
@@ -852,18 +658,16 @@ const app = new Elysia({
{}, {},
converterName, converterName,
); );
if (jobId.value) {
query.run(jobId.value, fileName, newFileName, result); query.run(jobId.value, fileName, newFileName, result);
}
}), }),
) )
.then(() => { .then(() => {
// All conversions are done, update the job status to 'completed' // All conversions are done, update the job status to 'completed'
if (jobId.value) { db.run(
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run( "UPDATE jobs SET status = 'completed' WHERE id = ?",
jobId.value, jobId.value,
); );
}
// delete all uploaded files in userUploadsDir // delete all uploaded files in userUploadsDir
// rmSync(userUploadsDir, { recursive: true, force: true }); // rmSync(userUploadsDir, { recursive: true, force: true });
@@ -873,7 +677,7 @@ const app = new Elysia({
}); });
// Redirect the client immediately // Redirect the client immediately
return redirect(`/results/${jobId.value}`, 302); return redirect(`/results/${jobId.value}`);
}, },
{ {
body: t.Object({ body: t.Object({
@@ -884,24 +688,22 @@ const app = new Elysia({
) )
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => { .get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
let userJobs = db let userJobs = db
.query("SELECT * FROM jobs WHERE user_id = ?") .query("SELECT * FROM jobs WHERE user_id = ?")
.as(Jobs) .all(user.id) as IJobs[];
.all(user.id);
for (const job of userJobs) { for (const job of userJobs) {
const files = db const files = db
.query("SELECT * FROM file_names WHERE job_id = ?") .query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename) .all(job.id) as IFileNames[];
.all(job.id);
job.finished_files = files.length; job.finished_files = files.length;
} }
@@ -911,7 +713,6 @@ const app = new Elysia({
return ( return (
<BaseHtml title="ConvertX | Results"> <BaseHtml title="ConvertX | Results">
<>
<Header loggedIn /> <Header loggedIn />
<main class="container"> <main class="container">
<article> <article>
@@ -943,7 +744,6 @@ const app = new Elysia({
</table> </table>
</article> </article>
</main> </main>
</>
</BaseHtml> </BaseHtml>
); );
}) })
@@ -951,7 +751,7 @@ const app = new Elysia({
"/results/:jobId", "/results/:jobId",
async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => { async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
if (job_id?.value) { if (job_id?.value) {
@@ -961,13 +761,12 @@ const app = new Elysia({
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
const job = db const job = (await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) .get(user.id, params.jobId)) as IJobs;
.get(user.id, params.jobId);
if (!job) { if (!job) {
set.status = 404; set.status = 404;
@@ -980,12 +779,10 @@ const app = new Elysia({
const files = db const files = db
.query("SELECT * FROM file_names WHERE job_id = ?") .query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename) .all(params.jobId) as IFileNames[];
.all(params.jobId);
return ( return (
<BaseHtml title="ConvertX | Result"> <BaseHtml title="ConvertX | Result">
<>
<Header loggedIn /> <Header loggedIn />
<main class="container"> <main class="container">
<article> <article>
@@ -1041,7 +838,6 @@ const app = new Elysia({
</article> </article>
</main> </main>
<script src="/results.js" defer /> <script src="/results.js" defer />
</>
</BaseHtml> </BaseHtml>
); );
}, },
@@ -1050,7 +846,7 @@ const app = new Elysia({
"/progress/:jobId", "/progress/:jobId",
async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => { async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
if (job_id?.value) { if (job_id?.value) {
@@ -1060,13 +856,12 @@ const app = new Elysia({
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
const job = db const job = (await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) .get(user.id, params.jobId)) as IJobs;
.get(user.id, params.jobId);
if (!job) { if (!job) {
set.status = 404; set.status = 404;
@@ -1079,8 +874,7 @@ const app = new Elysia({
const files = db const files = db
.query("SELECT * FROM file_names WHERE job_id = ?") .query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename) .all(params.jobId) as IFileNames[];
.all(params.jobId);
return ( return (
<article> <article>
@@ -1140,12 +934,12 @@ const app = new Elysia({
"/download/:userId/:jobId/:fileName", "/download/:userId/:jobId/:fileName",
async ({ params, jwt, redirect, cookie: { auth } }) => { async ({ params, jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
const job = await db const job = await db
@@ -1153,7 +947,7 @@ const app = new Elysia({
.get(user.id, params.jobId); .get(user.id, params.jobId);
if (!job) { if (!job) {
return redirect("/results", 302); return redirect("/results");
} }
// parse from url encoded string // parse from url encoded string
const userId = decodeURIComponent(params.userId); const userId = decodeURIComponent(params.userId);
@@ -1166,17 +960,16 @@ const app = new Elysia({
) )
.get("/converters", async ({ jwt, redirect, cookie: { auth } }) => { .get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
return ( return (
<BaseHtml title="ConvertX | Converters"> <BaseHtml title="ConvertX | Converters">
<>
<Header loggedIn /> <Header loggedIn />
<main class="container"> <main class="container">
<article> <article>
@@ -1190,8 +983,7 @@ const app = new Elysia({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Object.entries(getAllTargets()).map( {Object.entries(getAllTargets()).map(([converter, targets]) => {
([converter, targets]) => {
const inputs = getAllInputs(converter); const inputs = getAllInputs(converter);
return ( return (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation> // biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
@@ -1217,13 +1009,11 @@ const app = new Elysia({
</td> </td>
</tr> </tr>
); );
}, })}
)}
</tbody> </tbody>
</table> </table>
</article> </article>
</main> </main>
</>
</BaseHtml> </BaseHtml>
); );
}) })
@@ -1232,12 +1022,12 @@ const app = new Elysia({
async ({ params, jwt, redirect, cookie: { auth } }) => { async ({ params, jwt, redirect, cookie: { auth } }) => {
// TODO: Implement zip download // TODO: Implement zip download
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect("/login");
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect("/login");
} }
const job = await db const job = await db
@@ -1245,17 +1035,17 @@ const app = new Elysia({
.get(user.id, params.jobId); .get(user.id, params.jobId);
if (!job) { if (!job) {
return redirect("/results", 302); return redirect("/results");
} }
// const userId = decodeURIComponent(params.userId); const userId = decodeURIComponent(params.userId);
// const jobId = decodeURIComponent(params.jobId); const jobId = decodeURIComponent(params.jobId);
// const outputPath = `${outputDir}${userId}/${jobId}/`; const outputPath = `${outputDir}${userId}/${jobId}/`;
// return Bun.zip(outputPath); // return Bun.zip(outputPath);
}, },
) )
.onError(({ error }) => { .onError(({ code, error, request }) => {
// log.error(` ${request.method} ${request.url}`, code, error); // log.error(` ${request.method} ${request.url}`, code, error);
console.error(error); console.error(error);
}) })
@@ -1266,14 +1056,11 @@ console.log(
); );
const clearJobs = () => { const clearJobs = () => {
// clear all jobs older than 24 hours
// get all files older than 24 hours
const jobs = db const jobs = db
.query("SELECT * FROM jobs WHERE date_created < ?") .query("SELECT * FROM jobs WHERE date_created < ?")
.as(Jobs) .all(new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()) as IJobs[];
.all(
new Date(
Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000,
).toISOString(),
);
for (const job of jobs) { for (const job of jobs) {
// delete the directories // delete the directories
@@ -1284,9 +1071,7 @@ const clearJobs = () => {
db.query("DELETE FROM jobs WHERE id = ?").run(job.id); db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
} }
setTimeout(clearJobs, AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000); // run every 24 hours
setTimeout(clearJobs, 24 * 60 * 60 * 1000);
}; };
clearJobs();
if (AUTO_DELETE_EVERY_N_HOURS > 0) {
clearJobs();
}

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

@@ -3,73 +3,7 @@ const fileInput = document.querySelector('input[type="file"]');
const fileNames = []; const fileNames = [];
let fileType; let fileType;
const selectContainer = document.querySelector("form .select_container"); const selectContainer = document.querySelector("form > article");
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.hidden = false;
} else {
target.hidden = true;
}
}
if (matchingTargetsFound === 0) {
groupElement.hidden = true;
} else {
groupElement.hidden = false;
}
}
};
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}`;
showMatching("");
};
}
convertToGroups[groupName] = [targets, groupElement];
}
convertToInput.addEventListener("input", (e) => {
showMatching(e.target.value.toLowerCase());
});
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.hidden = true;
return;
}
convertToPopup.hidden = true;
});
convertToInput.addEventListener("focus", () => {
convertToPopup.hidden = false;
});
};
// const convertFromSelect = document.querySelector("select[name='convert_from']"); // const convertFromSelect = document.querySelector("select[name='convert_from']");
@@ -115,7 +49,6 @@ fileInput.addEventListener("change", (e) => {
.then((res) => res.text()) .then((res) => res.text())
.then((html) => { .then((html) => {
selectContainer.innerHTML = html; selectContainer.innerHTML = html;
updateSearchBar();
}) })
.catch((err) => console.log(err)); .catch((err) => console.log(err));
} }
@@ -190,5 +123,3 @@ formConvert.addEventListener("submit", (e) => {
const hiddenInput = document.querySelector("input[name='file_names']"); const hiddenInput = document.querySelector("input[name='file_names']");
hiddenInput.value = JSON.stringify(fileNames); hiddenInput.value = JSON.stringify(fileNames);
}); });
updateSearchBar();

View File

@@ -1,10 +1,16 @@
article {
/* height: 300px; */
/* width: 300px; */
}
div.icon { div.icon {
height: 100px; height: 100px;
width: 100px; width: 100px;
} }
button[type="submit"] { button[type="submit"] {
width: 50%; width: 50%
} }
div.center { div.center {
@@ -13,47 +19,3 @@ div.center {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
@media (max-width: 99999999999px) {
.convert_to_popup {
width: 50vw !important;
height: 50vh;
}
}
@media (max-width: 850px) {
.convert_to_popup {
width: 60vw !important;
height: 60vh;
}
}
@media (max-width: 575px) {
.convert_to_popup {
width: 80vw !important;
height: 75vh;
}
}
@media (max-height: 1000px) {
.convert_to_popup {
height: 40vh;
}
}
@media (max-height: 650px) {
.convert_to_popup {
height: 30vh;
}
}
@media (max-height: 500px) {
.convert_to_popup {
height: 25vh;
}
}
@media (max-height: 400px) {
.convert_to_popup {
height: 15vh;
}
}

View File

@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ESNext"], "lib": ["ESNext"],
"module": "ESNext", "module": "esnext",
"target": "ES2021", "target": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"moduleDetection": "force", "moduleDetection": "force",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@@ -17,6 +17,9 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"allowJs": true, "allowJs": true,
"types": [
"bun-types" // add Bun global
],
// non bun init // non bun init
"plugins": [{ "name": "@kitajs/ts-html-plugin" }], "plugins": [{ "name": "@kitajs/ts-html-plugin" }],
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,