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 27d33c8..1eeb66d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -10,7 +10,6 @@ on: branches: ["main"] workflow_dispatch: env: - GHCR_IMAGE: ghcr.io/c4illin/convertx IMAGE_NAME: ${{ github.repository }} DOCKERHUB_USERNAME: c4illin @@ -32,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' }} @@ -53,11 +51,15 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: downcase REPO + run: | + echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}" + - name: Docker meta default id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.GHCR_IMAGE }} + images: ghcr.io/${{ env.REPO }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -82,7 +84,8 @@ jobs: platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} - outputs: type=image,name=${{ env.GHCR_IMAGE }},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 }} @@ -101,30 +104,36 @@ 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-* merge-multiple: true + - name: downcase REPO + run: | + echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}" + - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: images: | - ${{ env.GHCR_IMAGE }} + ghcr.io/${{ env.REPO }} ${{ env.IMAGE_NAME }} - name: Set up Docker Buildx @@ -157,8 +166,8 @@ jobs: --annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \ --annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \ --annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \ - $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *) + $(printf 'ghcr.io/${{ env.REPO }}@sha256:%s ' *) - name: Inspect image run: | - docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}' + docker buildx imagetools inspect 'ghcr.io/${{ env.REPO }}:${{ steps.meta.outputs.version }}' diff --git a/.gitignore b/.gitignore index db36c85..c1fe8e3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ package-lock.json /output /db /data +/dist /Bruno /tsconfig.tsbuildinfo /public/generated.css 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/Dockerfile b/Dockerfile index 0711ef9..306c5bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -77,10 +77,13 @@ RUN apt-get update && apt-get install -y \ COPY --from=install /temp/prod/node_modules node_modules COPY --from=prerelease /app/public/generated.css /app/public/ -COPY . . +COPY --from=prerelease /app/dist /app/dist + +# COPY . . +RUN mkdir data EXPOSE 3000/tcp # used for calibre ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox" ENV NODE_ENV=production -ENTRYPOINT [ "bun", "run", "./src/index.tsx" ] +ENTRYPOINT [ "bun", "run", "dist/src/index.js" ] 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/eslint.config.ts b/eslint.config.ts index 6bf8f24..ebb2d16 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,12 +1,11 @@ import js from "@eslint/js"; import eslintParserTypeScript from "@typescript-eslint/parser"; -import type { Linter } from "eslint"; import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss"; import simpleImportSortPlugin from "eslint-plugin-simple-import-sort"; import globals from "globals"; import tseslint from "typescript-eslint"; -export default [ +export default tseslint.config( js.configs.recommended, ...tseslint.configs.recommended, // ...tailwind.configs["flat/recommended"], @@ -15,7 +14,7 @@ export default [ "simple-import-sort": simpleImportSortPlugin, "better-tailwindcss": eslintPluginBetterTailwindcss, }, - ignores: ["**/node_modules/**"], + ignores: ["**/node_modules/**", "eslint.config.ts"], languageOptions: { parser: eslintParserTypeScript, parserOptions: { @@ -26,10 +25,9 @@ export default [ }, globals: { ...globals.node, - ...globals.browser, }, }, - files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"], + files: ["**/*.{tsx,ts}"], settings: { "better-tailwindcss": { entryPoint: "src/main.css", @@ -63,4 +61,13 @@ export default [ ], }, }, -] as Linter.Config[]; + { + files: ["**/*.{js,cjs,mjs,jsx}"], + extends: [tseslint.configs.disableTypeChecked], + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, +); 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 4ac248a..620b47f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "format": "run-p 'format:*'", "format:eslint": "eslint --fix .", "format:prettier": "prettier --write .", - "build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css", + "build:js": "tsc", + "build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css && bun run build:js", "lint": "run-p 'lint:*'", "lint:tsc": "tsc --noEmit", "lint:knip": "knip", @@ -38,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/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/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 +} diff --git a/tsconfig.json b/tsconfig.json index 500a42a..234339b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,8 +5,9 @@ "target": "ES2021", "moduleResolution": "bundler", "moduleDetection": "force", - "allowImportingTsExtensions": true, - "noEmit": true, + // "allowImportingTsExtensions": true, + "outDir": "dist", + "noEmit": false, "composite": true, "strict": true, "downlevelIteration": true, @@ -24,7 +25,10 @@ // "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true + "noImplicitOverride": true, + "resolveJsonModule": true, + "esModuleInterop": true // "noImplicitReturns": true - } + }, + "include": ["src", "package.json", "reset.d.ts"] }