Merge branch 'main' into changes

This commit is contained in:
Emrik Östling 2025-08-08 00:19:50 +02:00 committed by GitHub
commit a85edb6cee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 140 additions and 81 deletions

View file

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

View file

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

View file

@ -3,8 +3,7 @@ name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: "[Feature Request]" title: "[Feature Request]"
labels: enhancement labels: enhancement
assignees: '' assignees: ""
--- ---
**Describe the solution you'd like** **Describe the solution you'd like**

31
.github/workflows/check-lint.yml vendored Normal file
View file

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

View file

@ -31,8 +31,7 @@ jobs:
contents: write contents: write
packages: write packages: write
attestations: write attestations: write
checks: write id-token: write
actions: read
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }} runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
@ -85,7 +84,8 @@ jobs:
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }} annotations: ${{ steps.meta.outputs.annotations }}
outputs: type=image,name=ghcr.io/${{ env.REPO }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true outputs: type=image,name=ghcr.io/${{ env.REPO }},push-by-digest=true,name-canonical=true,oci-mediatypes=true
push: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
cache-from: type=gha,scope=${{ matrix.platform }} cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }} cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
@ -104,19 +104,21 @@ jobs:
retention-days: 1 retention-days: 1
merge: merge:
if: github.event.pull_request.head.repo.full_name == github.repository
name: Merge Docker manifests name: Merge Docker manifests
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
attestations: write contents: write
contents: read
packages: write packages: write
attestations: write
id-token: write
needs: needs:
- build - build
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digests-* pattern: digests-*

View file

@ -2,10 +2,9 @@
## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04) ## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04)
### Bug Fixes ### Bug Fixes
* change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311) - change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311)
## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03) ## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03)

View file

@ -94,7 +94,6 @@ All are optional, JWT_SECRET is recommended to be set.
| LANGUAGE | en | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) | | LANGUAGE | en | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) |
| UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users | | UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users |
### Docker images ### Docker images
There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use. There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use.
@ -133,19 +132,10 @@ Tutorial in chinese: <https://xzllll.com/24092901/>
2. `bun install` 2. `bun install`
3. `bun run dev` 3. `bun run dev`
Pull requests are welcome! See below and open issues for the list of todos. Pull requests are welcome! See open issues for the list of todos. The ones tagged with "converter request" are quite easy. Help with docs and cleaning up in issues are also very welcome!
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages. Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
## Todo
- [ ] Add options for converters
- [ ] Add tests
- [ ] Make errors logs visible from the web ui
- [ ] Add more converters:
- [ ] [deark](https://github.com/jsummers/deark)
- [ ] LibreOffice
## Contributors ## Contributors
<a href="https://github.com/C4illin/ConvertX/graphs/contributors"> <a href="https://github.com/C4illin/ConvertX/graphs/contributors">

View file

@ -22,7 +22,6 @@
"@types/bun": "1.2.2", "@types/bun": "1.2.2",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@typescript-eslint/parser": "^8.34.0", "@typescript-eslint/parser": "^8.34.0",
"bun-types": "1.2.17",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-plugin-better-tailwindcss": "^3.1.0", "eslint-plugin-better-tailwindcss": "^3.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
@ -275,7 +274,7 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
@ -655,8 +654,6 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/bun/bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],

View file

@ -1,7 +1,6 @@
{ {
"$schema": "https://unpkg.com/knip@5/schema.json", "$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["src/index.tsx"], "project": ["src/**/*.ts", "src/**/*.tsx"],
"project": ["src/**/*.ts", "src/**/*.tsx", "src/main.css"],
"tailwind": { "tailwind": {
"entry": ["src/main.css"] "entry": ["src/main.css"]
}, },

View file

@ -39,7 +39,6 @@
"@types/bun": "1.2.2", "@types/bun": "1.2.2",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@typescript-eslint/parser": "^8.34.0", "@typescript-eslint/parser": "^8.34.0",
"bun-types": "1.2.17",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-plugin-better-tailwindcss": "^3.1.0", "eslint-plugin-better-tailwindcss": "^3.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",

View file

@ -4,5 +4,6 @@
"lockFileMaintenance": { "lockFileMaintenance": {
"enabled": true, "enabled": true,
"automerge": true "automerge": true
} },
"ignoreDeps": ["bun-types", "@types/bun"]
} }

View file

@ -66,10 +66,7 @@ export async function convert(
options?: unknown, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile( execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
"ebook-convert",
[filePath, targetPath],
(error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);
} }
@ -83,7 +80,6 @@ export async function convert(
} }
resolve("Done"); resolve("Done");
}, });
);
}); });
} }

View file

@ -1,3 +1,4 @@
import { Cookie } from "elysia";
import db from "../db/db"; import db from "../db/db";
import { MAX_CONVERT_PROCESS } from "../helpers/env"; import { MAX_CONVERT_PROCESS } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype"; import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
@ -119,11 +120,11 @@ const properties: Record<
}; };
function chunks<T>(arr: T[], size: number): T[][] { function chunks<T>(arr: T[], size: number): T[][] {
if(size <= 0){ if (size <= 0) {
return [arr] return [arr];
} }
return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) => return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) =>
arr.slice(i * size, i * size + size) arr.slice(i * size, i * size + size),
); );
} }
@ -133,17 +134,15 @@ export async function handleConvert(
userOutputDir: string, userOutputDir: string,
convertTo: string, convertTo: string,
converterName: string, converterName: string,
jobId: any jobId: Cookie<string | undefined>,
) { ) {
const query = db.query( const query = db.query(
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)", "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
); );
for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) { for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) {
const toProcess: Promise<string>[] = []; const toProcess: Promise<string>[] = [];
for(const fileName of chunk) { for (const fileName of chunk) {
const filePath = `${userUploadsDir}${fileName}`; const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop() ?? ""; const fileTypeOrig = fileName.split(".").pop() ?? "";
const fileType = normalizeFiletype(fileTypeOrig); const fileType = normalizeFiletype(fileTypeOrig);
@ -155,27 +154,22 @@ export async function handleConvert(
const targetPath = `${userOutputDir}${newFileName}`; const targetPath = `${userOutputDir}${newFileName}`;
toProcess.push( toProcess.push(
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
mainConverter( mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
filePath, .then((r) => {
fileType,
convertTo,
targetPath,
{},
converterName,
).then(r => {
if (jobId.value) { if (jobId.value) {
query.run(jobId.value, fileName, newFileName, r); query.run(jobId.value, fileName, newFileName, r);
} }
resolve(r); resolve(r);
}).catch(c => reject(c));
}) })
.catch((c) => reject(c));
}),
); );
} }
await Promise.all(toProcess); await Promise.all(toProcess);
} }
} }
export async function mainConverter( async function mainConverter(
inputFilePath: string, inputFilePath: string,
fileTypeOriginal: string, fileTypeOriginal: string,
convertTo: string, convertTo: string,

View file

@ -33,13 +33,19 @@ export function convert(
if (stderr) { if (stderr) {
// Log sanitized stderr to avoid exposing sensitive paths // Log sanitized stderr to avoid exposing sensitive paths
const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]"); const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]");
console.warn(`msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + '...' : sanitizedStderr}`); console.warn(
`msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`,
);
} }
resolve(targetPath); resolve(targetPath);
}); });
} else { } else {
reject(new Error(`Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`)); reject(
new Error(
`Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`,
),
);
} }
}); });
} }

View file

@ -16,7 +16,10 @@ export const WEBROOT = process.env.WEBROOT ?? "";
export const LANGUAGE = process.env.LANGUAGE?.toLowerCase() || "en"; export const LANGUAGE = process.env.LANGUAGE?.toLowerCase() || "en";
export const MAX_CONVERT_PROCESS = process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0 ? Number(process.env.MAX_CONVERT_PROCESS) : 0 export const MAX_CONVERT_PROCESS =
process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0
? Number(process.env.MAX_CONVERT_PROCESS)
: 0;
export const UNAUTHENTICATED_USER_SHARING = export const UNAUTHENTICATED_USER_SHARING =
process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false; process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false;

View file

@ -46,6 +46,11 @@ export const convert = new Elysia().use(userService).post(
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? ""); const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
const converterName = body.convert_to.split(",")[1]; const converterName = body.convert_to.split(",")[1];
if (!converterName) {
return redirect(`${WEBROOT}/`, 302);
}
const fileNames = JSON.parse(body.file_names) as string[]; const fileNames = JSON.parse(body.file_names) as string[];
for (let i = 0; i < fileNames.length; i++) { for (let i = 0; i < fileNames.length; i++) {

View file

@ -1,11 +1,11 @@
import path from "node:path";
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import * as tar from "tar";
import { outputDir } from ".."; import { outputDir } from "..";
import db from "../db/db"; import db from "../db/db";
import { WEBROOT } from "../helpers/env"; import { WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
import path from "node:path";
import * as tar from "tar";
export const download = new Elysia() export const download = new Elysia()
.use(userService) .use(userService)
@ -58,8 +58,17 @@ export const download = new Elysia()
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}`;
const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`) const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
await tar.create({file: outputTar, cwd: outputPath, filter: (path) => { return !path.match(".*\\.tar"); }}, ["."]); await tar.create(
{
file: outputTar,
cwd: outputPath,
filter: (path) => {
return !path.match(".*\\.tar");
},
},
["."],
);
return Bun.file(outputTar); return Bun.file(outputTar);
}); });

View file

@ -1,4 +1,5 @@
import { Html } from "@elysiajs/html"; import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Header } from "../components/header"; import { Header } from "../components/header";
@ -6,7 +7,6 @@ import db from "../db/db";
import { Filename, Jobs } from "../db/types"; import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
import { JWTPayloadSpec } from "@elysiajs/jwt";
function ResultsArticle({ function ResultsArticle({
user, user,

View file

@ -34,7 +34,9 @@ export const root = new Elysia()
let user: ({ id: string } & JWTPayloadSpec) | false = false; let user: ({ id: string } & JWTPayloadSpec) | false = false;
if (ALLOW_UNAUTHENTICATED) { if (ALLOW_UNAUTHENTICATED) {
const newUserId = String( const newUserId = String(
UNAUTHENTICATED_USER_SHARING ? 0 : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), UNAUTHENTICATED_USER_SHARING
? 0
: randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
); );
const accessToken = await jwt.sign({ const accessToken = await jwt.sign({
id: newUserId, id: newUserId,