Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0f30224b5 | ||
|
|
d0d888e356 | ||
|
|
2c64122224 | ||
|
|
3b2eee96a9 | ||
|
|
465aacbf9b | ||
|
|
d1a2a66170 | ||
|
|
4c05fd72bb | ||
|
|
f04fe760e3 | ||
|
|
834d19bcc6 | ||
|
|
6808c4642c | ||
|
|
d0ce307f94 | ||
|
|
f3740e9ded | ||
|
|
b485bc9445 | ||
|
|
2d14c1bb26 | ||
|
|
1a442d6e69 | ||
|
|
2386543e5c | ||
|
|
58e220e82d | ||
|
|
24bea6e4d2 | ||
|
|
43497ad8d1 | ||
|
|
f22b61fe4c | ||
|
|
5b08f4cd19 | ||
|
|
1589f8d24e | ||
|
|
7d1db72cf5 | ||
|
|
53a8f66414 | ||
|
|
36cb6cc589 | ||
|
|
f3a4aece46 | ||
|
|
580a6a869a | ||
|
|
008eaac493 | ||
|
|
b450623bb4 | ||
|
|
8ac2ecb673 | ||
|
|
0a10a56ae3 | ||
|
|
9378ba9208 | ||
|
|
0c586e324b | ||
|
|
91c4a64284 | ||
|
|
c599e98d9d | ||
|
|
d2cd6706c9 | ||
|
|
e8ed10dde8 | ||
|
|
5fe0b79802 | ||
|
|
34a6722a68 | ||
|
|
5b0d769c63 | ||
|
|
718401a28b | ||
|
|
3112cd57f6 | ||
|
|
410fc777a7 | ||
|
|
8eed99e732 |
@@ -1,16 +1,20 @@
|
||||
node_modules
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
Makefile
|
||||
helm-charts
|
||||
.env
|
||||
.editorconfig
|
||||
.idea
|
||||
.vscode
|
||||
CHANGELOG.md
|
||||
coverage*
|
||||
data
|
||||
docker-compose*
|
||||
Dockerfile*
|
||||
eslint.config.js
|
||||
helm-charts
|
||||
LICENSE
|
||||
Makefile
|
||||
node_modules
|
||||
prettier.config.js
|
||||
README.md
|
||||
renovate.json
|
||||
13
.github/workflows/docker-publish.yml
vendored
@@ -19,6 +19,7 @@ env:
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
DOCKERHUB_USERNAME: c4illin
|
||||
|
||||
|
||||
jobs:
|
||||
@@ -46,13 +47,22 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
@@ -67,3 +77,4 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
27
.github/workflows/dockerhub-description.yml
vendored
Normal file
@@ -0,0 +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@v4
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: ${{ env.IMAGE_NAME }}
|
||||
short-description: ${{ github.event.repository.description }}
|
||||
enable-url-completion: true
|
||||
2
.gitignore
vendored
@@ -48,4 +48,4 @@ package-lock.json
|
||||
/data
|
||||
/Bruno
|
||||
/tsconfig.tsbuildinfo
|
||||
/src/public/generated.css
|
||||
/public/generated.css
|
||||
|
||||
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1.1.29-alpine AS base
|
||||
FROM oven/bun:1.1.36-alpine AS base
|
||||
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
|
||||
WORKDIR /app
|
||||
|
||||
@@ -50,14 +50,15 @@ RUN apk --no-cache add \
|
||||
vips-poppler \
|
||||
vips-jxl \
|
||||
libjxl-tools \
|
||||
assimp
|
||||
assimp \
|
||||
inkscape
|
||||
|
||||
# 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=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
|
||||
COPY --from=prerelease /app/src/public/generated.css /app/src/public/
|
||||
COPY --from=prerelease /app/public/generated.css /app/public/
|
||||
# COPY --from=prerelease /app/src/index.tsx /app/src/
|
||||
# COPY --from=prerelease /app/package.json .
|
||||
COPY . .
|
||||
|
||||
37
README.md
@@ -1,11 +1,14 @@
|
||||

|
||||
|
||||
# ConvertX
|
||||
|
||||
[](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
|
||||
[](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX)
|
||||
[](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
|
||||

|
||||

|
||||

|
||||

|
||||
<!--  -->
|
||||
|
||||
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
|
||||
|
||||
@@ -27,6 +30,7 @@ A self-hosted online file converter. Supports over a thousand different formats.
|
||||
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
|
||||
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
||||
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
|
||||
| [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
|
||||
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 |
|
||||
|
||||
<!-- many ffmpeg fileformats are duplicates -->
|
||||
@@ -44,12 +48,8 @@ services:
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
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)
|
||||
- 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=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
|
||||
environment:
|
||||
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
|
||||
volumes:
|
||||
- convertx:/app/data
|
||||
```
|
||||
@@ -64,9 +64,27 @@ Then visit `http://localhost:3000` in your browser and create your account. Don'
|
||||
|
||||
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/" |
|
||||
|
||||
> [!WARNING]
|
||||
> If you can't login, make sure you are accessing the service over https or set HTTP_ALLOWED=true
|
||||
|
||||
### Tutorial
|
||||
|
||||
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
|
||||
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
|
||||
|
||||
Tutorial in chinese: <https://xzllll.com/24092901/>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -82,6 +100,7 @@ Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
|
||||
Pull requests are welcome! See below and open issues for the list of todos.
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] Add messages for errors in converters
|
||||
- [x] Add searchable list of formats
|
||||
- [ ] Add options for converters
|
||||
@@ -97,7 +116,7 @@ Pull requests are welcome! See below and open issues for the list of todos.
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" />
|
||||
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
|
||||
</a>
|
||||
|
||||
## Star History
|
||||
|
||||
@@ -11,5 +11,6 @@ services:
|
||||
- HTTP_ALLOWED=true # setting this to true is unsafe, only set this to true locally
|
||||
- ALLOW_UNAUTHENTICATED=true # 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
|
||||
- WEBROOT=/convertx # the root path of the web interface, leave empty to disable
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
||||
48
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "convertx-frontend",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.tsx",
|
||||
"hot": "bun run --hot src/index.tsx",
|
||||
"format": "eslint --fix .",
|
||||
"build": "postcss ./src/main.css -o ./src/public/generated.css",
|
||||
"build": "postcss ./src/main.css -o ./public/generated.css",
|
||||
"lint": "run-p 'lint:*'",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint:knip": "knip",
|
||||
@@ -13,10 +13,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/cookie": "^0.8.0",
|
||||
"@elysiajs/html": "1.0.2",
|
||||
"@elysiajs/html": "^1.1.1",
|
||||
"@elysiajs/jwt": "^1.1.1",
|
||||
"@elysiajs/static": "1.0.3",
|
||||
"elysia": "^1.1.17"
|
||||
"@elysiajs/static": "^1.1.1",
|
||||
"@kitajs/html": "^4.2.4",
|
||||
"elysia": "^1.1.25"
|
||||
},
|
||||
"module": "src/index.tsx",
|
||||
"type": "module",
|
||||
@@ -24,38 +25,31 @@
|
||||
"start": "bun run src/index.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.1.1",
|
||||
"@eslint/js": "^9.12.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
"@eslint/compat": "^1.2.3",
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||
"@kitajs/ts-html-plugin": "^4.1.0",
|
||||
"@total-typescript/ts-reset": "^0.6.1",
|
||||
"@types/bun": "^1.1.10",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/bun": "^1.1.13",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
"@types/node": "^22.9.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cssnano": "^7.0.6",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-deprecation": "^3.0.0",
|
||||
"eslint-plugin-isaacscript": "^4.0.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-readable-tailwind": "^1.8.1",
|
||||
"eslint-plugin-readable-tailwind": "^1.8.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-tailwindcss": "^3.17.4",
|
||||
"globals": "^15.9.0",
|
||||
"knip": "^5.30.6",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"postcss": "^8.4.47",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"globals": "^15.12.0",
|
||||
"knip": "^5.37.1",
|
||||
"npm-run-all2": "^7.0.1",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"postcss-lightningcss": "^1.0.1",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2",
|
||||
"typescript-eslint": "^8.8.0"
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.15.0"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
|
||||
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
|
||||
}
|
||||
}
|
||||
8
postcss.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import autoprefixer from "autoprefixer";
|
||||
import cssnano from "cssnano";
|
||||
import tailwind from "tailwindcss";
|
||||
import tailwindConfig from "./tailwind.config.js";
|
||||
|
||||
export default {
|
||||
plugins: [autoprefixer, tailwind(tailwindConfig), cssnano],
|
||||
};
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 960 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,3 +1,5 @@
|
||||
const webroot = document.querySelector("meta[name='webroot']").content;
|
||||
|
||||
window.downloadAll = function () {
|
||||
// Get all download links
|
||||
const downloadLinks = document.querySelectorAll("a[download]");
|
||||
@@ -18,7 +20,7 @@ let progressElem = document.querySelector("progress");
|
||||
const refreshData = () => {
|
||||
// console.log("Refreshing data...", progressElem.value, progressElem.max);
|
||||
if (progressElem.value !== progressElem.max) {
|
||||
fetch(`/progress/${jobId}`, {
|
||||
fetch(`${webroot}/progress/${jobId}`, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((res) => res.text())
|
||||
@@ -1,8 +1,11 @@
|
||||
// Select the file input element
|
||||
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", () => {
|
||||
dropZone.classList.add("dragover");
|
||||
@@ -12,6 +15,10 @@ dropZone.addEventListener("dragleave", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("drop", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
const selectContainer = document.querySelector("form .select_container");
|
||||
|
||||
const updateSearchBar = () => {
|
||||
@@ -22,7 +29,6 @@ const updateSearchBar = () => {
|
||||
const convertToGroupElements = document.querySelectorAll(".convert_to_group");
|
||||
const convertToGroups = {};
|
||||
const convertToElement = document.querySelector("select[name='convert_to']");
|
||||
const convertButton = document.querySelector("input[type='submit']");
|
||||
|
||||
const showMatching = (search) => {
|
||||
for (const [targets, groupElement] of Object.values(convertToGroups)) {
|
||||
@@ -58,7 +64,10 @@ const updateSearchBar = () => {
|
||||
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("");
|
||||
};
|
||||
}
|
||||
@@ -73,6 +82,7 @@ const updateSearchBar = () => {
|
||||
convertToInput.addEventListener("search", () => {
|
||||
// when the user clears the search bar using the 'x' button
|
||||
convertButton.disabled = true;
|
||||
formatSelected = false;
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("blur", (e) => {
|
||||
@@ -94,11 +104,8 @@ const updateSearchBar = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// const convertFromSelect = document.querySelector("select[name='convert_from']");
|
||||
|
||||
// Add a 'change' event listener to the file input element
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
// console.log(e.target.files);
|
||||
// Get the selected files from the event target
|
||||
const files = e.target.files;
|
||||
|
||||
@@ -112,12 +119,11 @@ fileInput.addEventListener("change", (e) => {
|
||||
row.innerHTML = `
|
||||
<td>${file.name}</td>
|
||||
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
||||
<td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td>
|
||||
<td><a onclick="deleteRow(this)">Remove</a></td>
|
||||
`;
|
||||
|
||||
if (!fileType) {
|
||||
fileType = file.name.split(".").pop();
|
||||
console.log("fileType", fileType);
|
||||
fileInput.setAttribute("accept", `.${fileType}`);
|
||||
setTitle();
|
||||
|
||||
@@ -129,7 +135,7 @@ fileInput.addEventListener("change", (e) => {
|
||||
// }
|
||||
// }
|
||||
|
||||
fetch("/conversions", {
|
||||
fetch(`${webroot}/conversions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ fileType: fileType }),
|
||||
headers: {
|
||||
@@ -170,46 +176,57 @@ const deleteRow = (target) => {
|
||||
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("/delete", {
|
||||
fetch(`${webroot}/delete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ filename: filename }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const uploadFiles = (files) => {
|
||||
convertButton.disabled = true;
|
||||
convertButton.textContent = "Uploading...";
|
||||
pendingFiles += 1;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
for (const file of files) {
|
||||
formData.append("file", file, file.name);
|
||||
}
|
||||
|
||||
fetch("/upload", {
|
||||
fetch(`${webroot}/upload`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
pendingFiles -= 1;
|
||||
if (pendingFiles === 0) {
|
||||
if (formatSelected) {
|
||||
convertButton.disabled = false;
|
||||
}
|
||||
convertButton.textContent = "Convert";
|
||||
}
|
||||
console.log(data);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const formConvert = document.querySelector("form[action='/convert']");
|
||||
const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
|
||||
|
||||
formConvert.addEventListener("submit", () => {
|
||||
const hiddenInput = document.querySelector("input[name='file_names']");
|
||||
@@ -3,5 +3,9 @@
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":disableDependencyDashboard"
|
||||
]
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"automerge": true
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,39 @@
|
||||
import { Html } from "@elysiajs/html";
|
||||
|
||||
export const BaseHtml = ({
|
||||
children,
|
||||
title = "ConvertX",
|
||||
}: { children: JSX.Element; title?: string }) => (
|
||||
webroot = "",
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
title?: string;
|
||||
webroot?: string;
|
||||
}) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="webroot" content={webroot} />
|
||||
<title safe>{title}</title>
|
||||
<link rel="stylesheet" href="/generated.css" />
|
||||
<link rel="stylesheet" href={`${webroot}/generated.css`} />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
href={`${webroot}/apple-touch-icon.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
href={`${webroot}/favicon-32x32.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
href={`${webroot}/favicon-16x16.png`}
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="manifest" href={`${webroot}/site.webmanifest`} />
|
||||
</head>
|
||||
<body class="w-full bg-neutral-900 text-neutral-200">{children}</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Html } from "@kitajs/html";
|
||||
|
||||
export const Header = ({
|
||||
loggedIn,
|
||||
accountRegistration,
|
||||
}: { loggedIn?: boolean; accountRegistration?: boolean }) => {
|
||||
webroot = "",
|
||||
}: {
|
||||
loggedIn?: boolean;
|
||||
accountRegistration?: boolean;
|
||||
webroot?: string;
|
||||
}) => {
|
||||
let rightNav: JSX.Element;
|
||||
if (loggedIn) {
|
||||
rightNav = (
|
||||
@@ -12,7 +19,8 @@ export const Header = ({
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href="/history">
|
||||
href={`${webroot}/history`}
|
||||
>
|
||||
History
|
||||
</a>
|
||||
</li>
|
||||
@@ -22,7 +30,8 @@ export const Header = ({
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href="/logoff">
|
||||
href={`${webroot}/logoff`}
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</li>
|
||||
@@ -37,7 +46,8 @@ export const Header = ({
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href="/login">
|
||||
href={`${webroot}/login`}
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
@@ -48,7 +58,8 @@ export const Header = ({
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href="/register">
|
||||
href={`${webroot}/register`}
|
||||
>
|
||||
Register
|
||||
</a>
|
||||
</li>
|
||||
@@ -63,7 +74,7 @@ export const Header = ({
|
||||
<ul>
|
||||
<li>
|
||||
<strong>
|
||||
<a href="/">ConvertX</a>
|
||||
<a href={`${webroot}/`}>ConvertX</a>
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -11,6 +11,7 @@ export const properties = {
|
||||
"ac3d",
|
||||
"acc",
|
||||
"amf",
|
||||
"amj",
|
||||
"ase",
|
||||
"ask",
|
||||
"assbin",
|
||||
@@ -26,6 +27,7 @@ export const properties = {
|
||||
"fbx",
|
||||
"glb",
|
||||
"gltf",
|
||||
"hmb",
|
||||
"hmp",
|
||||
"ifc",
|
||||
"ifczip",
|
||||
@@ -35,6 +37,7 @@ export const properties = {
|
||||
"lwo",
|
||||
"lws",
|
||||
"lxo",
|
||||
"m3d",
|
||||
"md2",
|
||||
"md3",
|
||||
"md5anim",
|
||||
@@ -95,8 +98,7 @@ export const properties = {
|
||||
"glb2",
|
||||
"gltf",
|
||||
"gltf2",
|
||||
"m3d",
|
||||
"m3da",
|
||||
"json",
|
||||
"obj",
|
||||
"objnomtl",
|
||||
"pbrt",
|
||||
@@ -106,7 +108,6 @@ export const properties = {
|
||||
"stlb",
|
||||
"stp",
|
||||
"x",
|
||||
"x3d",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
64
src/converters/inkscape.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
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,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
||||
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
|
||||
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
|
||||
import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick";
|
||||
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
|
||||
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
|
||||
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
|
||||
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
|
||||
@@ -63,6 +64,10 @@ const properties: Record<
|
||||
properties: propertiesGraphicsmagick,
|
||||
converter: convertGraphicsmagick,
|
||||
},
|
||||
inkscape: {
|
||||
properties: propertiesInkscape,
|
||||
converter: convertInkscape,
|
||||
},
|
||||
assimp: {
|
||||
properties: propertiesassimp,
|
||||
converter: convertassimp,
|
||||
|
||||
@@ -11,6 +11,8 @@ export const normalizeFiletype = (filetype: string): string => {
|
||||
return "latex";
|
||||
case "md":
|
||||
return "markdown";
|
||||
case "unknown":
|
||||
return "m4a";
|
||||
default:
|
||||
return lowercaseFiletype;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,16 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
});
|
||||
|
||||
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.");
|
||||
|
||||
157
src/index.tsx
@@ -2,7 +2,7 @@ import { randomInt, randomUUID } from "node:crypto";
|
||||
import { rmSync } from "node:fs";
|
||||
import { mkdir, unlink } from "node:fs/promises";
|
||||
import cookie from "@elysiajs/cookie";
|
||||
import { html } from "@elysiajs/html";
|
||||
import { html, Html } from "@elysiajs/html";
|
||||
import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt";
|
||||
import { staticPlugin } from "@elysiajs/static";
|
||||
import { Database } from "bun:sqlite";
|
||||
@@ -37,6 +37,8 @@ const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
|
||||
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
|
||||
: 24;
|
||||
|
||||
const WEBROOT = process.env.WEBROOT ?? "";
|
||||
|
||||
// fileNames: fileNames,
|
||||
// filesToConvert: fileNames.length,
|
||||
// convertedFiles : 0,
|
||||
@@ -112,6 +114,7 @@ const app = new Elysia({
|
||||
serve: {
|
||||
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
prefix: WEBROOT,
|
||||
})
|
||||
.use(cookie())
|
||||
.use(html())
|
||||
@@ -127,24 +130,36 @@ const app = new Elysia({
|
||||
)
|
||||
.use(
|
||||
staticPlugin({
|
||||
assets: "src/public/",
|
||||
prefix: "/",
|
||||
assets: "public",
|
||||
prefix: "",
|
||||
}),
|
||||
)
|
||||
.get("/test", () => {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello World</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello</h1>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
})
|
||||
.get("/setup", ({ redirect }) => {
|
||||
if (!FIRST_RUN) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Setup">
|
||||
<BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
|
||||
<main class="mx-auto w-full max-w-4xl px-4">
|
||||
<h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
|
||||
<article class="article p-0">
|
||||
<header class="w-full bg-neutral-800 p-4">
|
||||
Create your account
|
||||
</header>
|
||||
<form method="post" action="/register" class="p-4">
|
||||
<form method="post" action={`${WEBROOT}/register`} class="p-4">
|
||||
<fieldset class="mb-4 flex flex-col gap-4">
|
||||
<label class="flex flex-col gap-1">
|
||||
Email
|
||||
@@ -191,13 +206,16 @@ const app = new Elysia({
|
||||
})
|
||||
.get("/register", ({ redirect }) => {
|
||||
if (!ACCOUNT_REGISTRATION) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Register">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Register">
|
||||
<>
|
||||
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<form method="post" class="flex flex-col gap-4">
|
||||
@@ -241,7 +259,7 @@ const app = new Elysia({
|
||||
"/register",
|
||||
async ({ body, set, redirect, jwt, cookie: { auth } }) => {
|
||||
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (FIRST_RUN) {
|
||||
@@ -296,13 +314,13 @@ const app = new Elysia({
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
},
|
||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||
)
|
||||
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (FIRST_RUN) {
|
||||
return redirect("/setup", 302);
|
||||
return redirect(`${WEBROOT}/setup`, 302);
|
||||
}
|
||||
|
||||
// if already logged in, redirect to home
|
||||
@@ -310,16 +328,19 @@ const app = new Elysia({
|
||||
const user = await jwt.verify(auth.value);
|
||||
|
||||
if (user) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
auth.remove();
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Login">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Login">
|
||||
<>
|
||||
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<form method="post" class="flex flex-col gap-4">
|
||||
@@ -350,7 +371,7 @@ const app = new Elysia({
|
||||
<div role="group">
|
||||
{ACCOUNT_REGISTRATION ? (
|
||||
<a
|
||||
href="/register"
|
||||
href={`${WEBROOT}/register`}
|
||||
role="button"
|
||||
class="btn-primary w-full"
|
||||
>
|
||||
@@ -417,7 +438,7 @@ const app = new Elysia({
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
},
|
||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||
)
|
||||
@@ -426,22 +447,22 @@ const app = new Elysia({
|
||||
auth.remove();
|
||||
}
|
||||
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
})
|
||||
.post("/logoff", ({ redirect, cookie: { auth } }) => {
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
})
|
||||
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
||||
if (FIRST_RUN) {
|
||||
return redirect("/setup", 302);
|
||||
return redirect(`${WEBROOT}/setup`, 302);
|
||||
}
|
||||
|
||||
if (!auth?.value && !ALLOW_UNAUTHENTICATED) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
// validate jwt
|
||||
@@ -461,7 +482,7 @@ const app = new Elysia({
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,7 +515,7 @@ const app = new Elysia({
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
// create a new job
|
||||
@@ -524,9 +545,9 @@ const app = new Elysia({
|
||||
console.log("jobId set to:", id);
|
||||
|
||||
return (
|
||||
<BaseHtml>
|
||||
<BaseHtml webroot={WEBROOT}>
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<Header webroot={WEBROOT} loggedIn />
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Convert</h1>
|
||||
@@ -562,7 +583,7 @@ const app = new Elysia({
|
||||
</article>
|
||||
<form
|
||||
method="post"
|
||||
action="/convert"
|
||||
action={`${WEBROOT}/convert`}
|
||||
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
|
||||
>
|
||||
<input type="hidden" name="file_names" id="file_names" />
|
||||
@@ -727,16 +748,16 @@ const app = new Elysia({
|
||||
"/upload",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
@@ -744,7 +765,7 @@ const app = new Elysia({
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -769,16 +790,16 @@ const app = new Elysia({
|
||||
"/delete",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
@@ -786,7 +807,7 @@ const app = new Elysia({
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -799,16 +820,16 @@ const app = new Elysia({
|
||||
"/convert",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = db
|
||||
@@ -817,7 +838,7 @@ const app = new Elysia({
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -838,7 +859,7 @@ const app = new Elysia({
|
||||
const fileNames = JSON.parse(body.file_names) as string[];
|
||||
|
||||
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
db.query(
|
||||
@@ -891,7 +912,7 @@ const app = new Elysia({
|
||||
});
|
||||
|
||||
// Redirect the client immediately
|
||||
return redirect(`/results/${jobId.value}`, 302);
|
||||
return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
@@ -902,12 +923,12 @@ const app = new Elysia({
|
||||
)
|
||||
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
const user = await jwt.verify(auth.value);
|
||||
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
let userJobs = db
|
||||
@@ -928,9 +949,9 @@ const app = new Elysia({
|
||||
userJobs = userJobs.filter((job) => job.num_files > 0);
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Results">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Results">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<Header webroot={WEBROOT} loggedIn />
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Results</h1>
|
||||
@@ -963,7 +984,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/results/${job.id}`}
|
||||
href={`${WEBROOT}/results/${job.id}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
@@ -982,7 +1003,7 @@ const app = new Elysia({
|
||||
"/results/:jobId",
|
||||
async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (job_id?.value) {
|
||||
@@ -992,7 +1013,7 @@ const app = new Elysia({
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const job = db
|
||||
@@ -1015,9 +1036,9 @@ const app = new Elysia({
|
||||
.all(params.jobId);
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Result">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<Header webroot={WEBROOT} loggedIn />
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
@@ -1075,7 +1096,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/download/${outputPath}${file.output_file_name}`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
@@ -1086,7 +1107,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/download/${outputPath}${file.output_file_name}`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
download={file.output_file_name}
|
||||
>
|
||||
Download
|
||||
@@ -1098,7 +1119,7 @@ const app = new Elysia({
|
||||
</table>
|
||||
</article>
|
||||
</main>
|
||||
<script src="/results.js" defer />
|
||||
<script src={`${WEBROOT}/results.js`} defer />
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
@@ -1108,7 +1129,7 @@ const app = new Elysia({
|
||||
"/progress/:jobId",
|
||||
async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (job_id?.value) {
|
||||
@@ -1118,7 +1139,7 @@ const app = new Elysia({
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const job = db
|
||||
@@ -1197,7 +1218,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/download/${outputPath}${file.output_file_name}`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
@@ -1208,7 +1229,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/download/${outputPath}${file.output_file_name}`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
download={file.output_file_name}
|
||||
>
|
||||
Download
|
||||
@@ -1226,12 +1247,12 @@ const app = new Elysia({
|
||||
"/download/:userId/:jobId/:fileName",
|
||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const job = await db
|
||||
@@ -1239,7 +1260,7 @@ const app = new Elysia({
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect("/results", 302);
|
||||
return redirect(`${WEBROOT}/results`, 302);
|
||||
}
|
||||
// parse from url encoded string
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
@@ -1252,18 +1273,18 @@ const app = new Elysia({
|
||||
)
|
||||
.get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Converters">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<Header webroot={WEBROOT} loggedIn />
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Converters</h1>
|
||||
@@ -1322,12 +1343,12 @@ const app = new Elysia({
|
||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||
// TODO: Implement zip download
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const job = await db
|
||||
@@ -1335,12 +1356,12 @@ const app = new Elysia({
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect("/results", 302);
|
||||
return redirect(`${WEBROOT}/results`, 302);
|
||||
}
|
||||
|
||||
// const userId = decodeURIComponent(params.userId);
|
||||
// const jobId = decodeURIComponent(params.jobId);
|
||||
// const outputPath = `${outputDir}${userId}/${jobId}/`;
|
||||
// const outputPath = `${outputDir}${userId}/`{jobId}/);
|
||||
|
||||
// return Bun.zip(outputPath);
|
||||
},
|
||||
@@ -1364,7 +1385,7 @@ if (process.env.NODE_ENV !== "production") {
|
||||
app.listen(3000);
|
||||
|
||||
console.log(
|
||||
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`,
|
||||
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`,
|
||||
);
|
||||
|
||||
const clearJobs = () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
module.exports = {
|
||||
import tailwindScrollbar from "tailwind-scrollbar";
|
||||
|
||||
export default {
|
||||
content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"],
|
||||
theme: {
|
||||
extend: {
|
||||
@@ -22,5 +23,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwind-scrollbar")],
|
||||
plugins: [tailwindScrollbar],
|
||||
};
|
||||
|
||||