diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 175d11a..d6ed108 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- **Describe the bug** @@ -12,10 +11,12 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Checklist:** + - [ ] I am accessing ConvertX over HTTPS or have `HTTP_ALLOWED=true` diff --git a/.github/ISSUE_TEMPLATE/converter_request.md b/.github/ISSUE_TEMPLATE/converter_request.md new file mode 100644 index 0000000..7de554d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/converter_request.md @@ -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?** + + + +**What converter should be added** + + + +**Are you willing to add it?** + + + +- [ ] Yes +- [ ] No + +**Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7e3bd6b..5140554 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,8 +3,7 @@ name: Feature request about: Suggest an idea for this project title: "[Feature Request]" labels: enhancement -assignees: '' - +assignees: "" --- **Describe the solution you'd like** diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml new file mode 100644 index 0000000..353903a --- /dev/null +++ b/.github/workflows/check-lint.yml @@ -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 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 8ae6e02..1eeb66d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -31,8 +31,7 @@ jobs: contents: write packages: write attestations: write - checks: write - actions: read + id-token: write 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 }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} - outputs: type=image,name=ghcr.io/${{ env.REPO }},push-by-digest=true,name-canonical=true,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-to: type=gha,mode=max,scope=${{ matrix.platform }} @@ -104,19 +104,21 @@ jobs: retention-days: 1 merge: + if: github.event.pull_request.head.repo.full_name == github.repository name: Merge Docker manifests runs-on: ubuntu-latest permissions: - attestations: write - contents: read + contents: write packages: write + attestations: write + id-token: write needs: - build steps: - name: Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: path: /tmp/digests pattern: digests-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 0654d68..49b69ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,9 @@ ## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04) - ### Bug Fixes -* change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311) +- 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) diff --git a/README.md b/README.md index 0d65690..446a4e9 100644 --- a/README.md +++ b/README.md @@ -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) | | UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users | - ### Docker images There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use. @@ -133,19 +132,10 @@ Tutorial in chinese: 2. `bun install` 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. -## 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 diff --git a/bun.lock b/bun.lock index 4bdeb22..b1c5902 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,6 @@ "@types/bun": "1.2.2", "@types/node": "^24.0.0", "@typescript-eslint/parser": "^8.34.0", - "bun-types": "1.2.17", "eslint": "^9.28.0", "eslint-plugin-better-tailwindcss": "^3.1.0", "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=="], - "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=="], @@ -655,8 +654,6 @@ "@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/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], diff --git a/knip.json b/knip.json index 0fc4de8..c531436 100644 --- a/knip.json +++ b/knip.json @@ -1,7 +1,6 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "entry": ["src/index.tsx"], - "project": ["src/**/*.ts", "src/**/*.tsx", "src/main.css"], + "project": ["src/**/*.ts", "src/**/*.tsx"], "tailwind": { "entry": ["src/main.css"] }, diff --git a/package.json b/package.json index 571ad9f..620b47f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "@types/bun": "1.2.2", "@types/node": "^24.0.0", "@typescript-eslint/parser": "^8.34.0", - "bun-types": "1.2.17", "eslint": "^9.28.0", "eslint-plugin-better-tailwindcss": "^3.1.0", "eslint-plugin-simple-import-sort": "^12.1.1", diff --git a/renovate.json b/renovate.json index 3418a44..ec32b06 100644 --- a/renovate.json +++ b/renovate.json @@ -4,5 +4,6 @@ "lockFileMaintenance": { "enabled": true, "automerge": true - } + }, + "ignoreDeps": ["bun-types", "@types/bun"] } diff --git a/src/converters/calibre.ts b/src/converters/calibre.ts index 8ebdbd1..f217b07 100644 --- a/src/converters/calibre.ts +++ b/src/converters/calibre.ts @@ -66,24 +66,20 @@ export async function convert( options?: unknown, ): Promise { return new Promise((resolve, reject) => { - execFile( - "ebook-convert", - [filePath, targetPath], - (error, stdout, stderr) => { - if (error) { - reject(`error: ${error}`); - } + execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => { + if (error) { + reject(`error: ${error}`); + } - if (stdout) { - console.log(`stdout: ${stdout}`); - } + if (stdout) { + console.log(`stdout: ${stdout}`); + } - if (stderr) { - console.error(`stderr: ${stderr}`); - } + if (stderr) { + console.error(`stderr: ${stderr}`); + } - resolve("Done"); - }, - ); + resolve("Done"); + }); }); } diff --git a/src/converters/main.ts b/src/converters/main.ts index 63106ff..8c59e55 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -1,3 +1,4 @@ +import { Cookie } from "elysia"; import db from "../db/db"; import { MAX_CONVERT_PROCESS } from "../helpers/env"; import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype"; @@ -119,11 +120,11 @@ const properties: Record< }; function chunks(arr: T[], size: number): T[][] { - if(size <= 0){ - return [arr] + if (size <= 0) { + return [arr]; } 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, convertTo: string, converterName: string, - jobId: any + jobId: Cookie, ) { - const query = db.query( "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)", ); - for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) { const toProcess: Promise[] = []; - for(const fileName of chunk) { + for (const fileName of chunk) { const filePath = `${userUploadsDir}${fileName}`; const fileTypeOrig = fileName.split(".").pop() ?? ""; const fileType = normalizeFiletype(fileTypeOrig); @@ -154,28 +153,23 @@ export async function handleConvert( ); const targetPath = `${userOutputDir}${newFileName}`; toProcess.push( - new Promise((resolve, reject) => { - mainConverter( - filePath, - fileType, - convertTo, - targetPath, - {}, - converterName, - ).then(r => { + new Promise((resolve, reject) => { + mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName) + .then((r) => { if (jobId.value) { query.run(jobId.value, fileName, newFileName, r); } resolve(r); - }).catch(c => reject(c)); - }) + }) + .catch((c) => reject(c)); + }), ); } await Promise.all(toProcess); } } -export async function mainConverter( +async function mainConverter( inputFilePath: string, fileTypeOriginal: string, convertTo: string, diff --git a/src/converters/msgconvert.ts b/src/converters/msgconvert.ts index b6bf826..8e95496 100644 --- a/src/converters/msgconvert.ts +++ b/src/converters/msgconvert.ts @@ -23,7 +23,7 @@ export function convert( // msgconvert will output to the same directory as the input file with .eml extension // We need to use --outfile to specify the target path const args = ["--outfile", targetPath, filePath]; - + execFile("msgconvert", args, (error, stdout, stderr) => { if (error) { reject(new Error(`msgconvert failed: ${error.message}`)); @@ -33,13 +33,19 @@ export function convert( if (stderr) { // Log sanitized stderr to avoid exposing sensitive paths const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]"); - console.warn(`msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + '...' : sanitizedStderr}`); + console.warn( + `msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`, + ); } resolve(targetPath); }); } else { - reject(new Error(`Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`)); + reject( + new Error( + `Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`, + ), + ); } }); } diff --git a/src/helpers/env.ts b/src/helpers/env.ts index 6722c54..4c8c067 100644 --- a/src/helpers/env.ts +++ b/src/helpers/env.ts @@ -16,7 +16,10 @@ export const WEBROOT = process.env.WEBROOT ?? ""; export const LANGUAGE = process.env.LANGUAGE?.toLowerCase() || "en"; -export const MAX_CONVERT_PROCESS = process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0 ? Number(process.env.MAX_CONVERT_PROCESS) : 0 +export const MAX_CONVERT_PROCESS = + process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0 + ? Number(process.env.MAX_CONVERT_PROCESS) + : 0; export const UNAUTHENTICATED_USER_SHARING = - process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false; \ No newline at end of file + process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false; diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index 3dbb892..fe4c325 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -46,6 +46,11 @@ export const convert = new Elysia().use(userService).post( const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? ""); const converterName = body.convert_to.split(",")[1]; + + if (!converterName) { + return redirect(`${WEBROOT}/`, 302); + } + const fileNames = JSON.parse(body.file_names) as string[]; for (let i = 0; i < fileNames.length; i++) { diff --git a/src/pages/download.tsx b/src/pages/download.tsx index 3333396..e5d7576 100644 --- a/src/pages/download.tsx +++ b/src/pages/download.tsx @@ -1,11 +1,11 @@ +import path from "node:path"; import { Elysia } from "elysia"; import sanitize from "sanitize-filename"; +import * as tar from "tar"; import { outputDir } from ".."; import db from "../db/db"; import { WEBROOT } from "../helpers/env"; import { userService } from "./user"; -import path from "node:path"; -import * as tar from "tar"; export const download = new Elysia() .use(userService) @@ -58,8 +58,17 @@ export const download = new Elysia() const userId = decodeURIComponent(params.userId); const jobId = decodeURIComponent(params.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); }); diff --git a/src/pages/results.tsx b/src/pages/results.tsx index edd70e7..c881f55 100644 --- a/src/pages/results.tsx +++ b/src/pages/results.tsx @@ -1,4 +1,5 @@ import { Html } from "@elysiajs/html"; +import { JWTPayloadSpec } from "@elysiajs/jwt"; import { Elysia } from "elysia"; import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; @@ -6,7 +7,6 @@ import db from "../db/db"; import { Filename, Jobs } from "../db/types"; import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { userService } from "./user"; -import { JWTPayloadSpec } from "@elysiajs/jwt"; function ResultsArticle({ user, @@ -26,7 +26,7 @@ function ResultsArticle({

Results

- + > {files.length === job.num_files ? "Download All" : "Converting..."} diff --git a/src/pages/root.tsx b/src/pages/root.tsx index b8aaf1b..fa4c8a8 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -34,7 +34,9 @@ export const root = new Elysia() let user: ({ id: string } & JWTPayloadSpec) | false = false; if (ALLOW_UNAUTHENTICATED) { const newUserId = String( - UNAUTHENTICATED_USER_SHARING ? 0 : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), + UNAUTHENTICATED_USER_SHARING + ? 0 + : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), ); const accessToken = await jwt.sign({ id: newUserId, diff --git a/src/theme/theme.css b/src/theme/theme.css index cc55666..34a0051 100644 --- a/src/theme/theme.css +++ b/src/theme/theme.css @@ -44,4 +44,4 @@ /* lime-400 */ --accent-400: oklch(84.1% 0.238 128.85); } -} \ No newline at end of file +}