From 79fc6f7067976ae011bd6c2e7b0646a67302eac8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 21 Jan 2026 16:00:26 +0800 Subject: [PATCH] fix: resolve lint issues (XSS K601, knip unused exports, eslint errors) - Fix XSS warnings in user.tsx by adding safe attribute to elements - Remove unused exports: ChunkUploadRequest, getArchiveInfo, getFileForDirectDownload, createDirectDownloadHeaders, createConverterArchive - Fix eslint no-unused-vars errors across multiple files - Fix pdfmathtranslate async Promise executor issue - Update tests to use toThrow instead of toMatch for Error objects - Apply prettier formatting --- public/script.js | 1 - public/theme.js | 2 +- src/components/themeToggle.tsx | 8 +- src/converters/main.ts | 5 +- src/converters/mineru.ts | 48 ++-- src/converters/pdfmathtranslate.ts | 262 +++++++++++----------- src/pages/download.tsx | 7 +- src/pages/downloadChunk.tsx | 27 +-- src/pages/uploadChunk.tsx | 11 +- src/pages/user.tsx | 30 ++- src/transfer/archiveManager.ts | 66 ++---- src/transfer/constants.ts | 2 +- src/transfer/downloadManager.ts | 35 +-- src/transfer/index.ts | 7 +- src/transfer/types.ts | 18 -- src/transfer/uploadManager.ts | 47 ++-- tests/converters/mineru.test.ts | 34 ++- tests/converters/pdfmathtranslate.test.ts | 55 ++--- tests/transfer/chunk-download.test.ts | 11 +- tests/transfer/chunk-upload.test.ts | 167 +++++++++++--- tests/transfer/transfer.test.ts | 35 ++- 21 files changed, 439 insertions(+), 439 deletions(-) diff --git a/public/script.js b/public/script.js index 7bd5d85..e7652a3 100644 --- a/public/script.js +++ b/public/script.js @@ -1,6 +1,5 @@ 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; diff --git a/public/theme.js b/public/theme.js index 41f277d..797998c 100644 --- a/public/theme.js +++ b/public/theme.js @@ -72,7 +72,7 @@ applyTheme(preferredTheme); // Listen for system preference changes - window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => { + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { // Only update if no manual preference is set if (!localStorage.getItem(THEME_KEY)) { applyTheme(null); diff --git a/src/components/themeToggle.tsx b/src/components/themeToggle.tsx index 569694a..a0c238e 100644 --- a/src/components/themeToggle.tsx +++ b/src/components/themeToggle.tsx @@ -1,7 +1,11 @@ -import { type SupportedLocale, type Translator, createTranslator, defaultLocale } from "../i18n/index"; +import { + type SupportedLocale, + type Translator, + createTranslator, + defaultLocale, +} from "../i18n/index"; export const ThemeToggle = ({ - locale = defaultLocale, t = createTranslator(defaultLocale), }: { locale?: SupportedLocale; diff --git a/src/converters/main.ts b/src/converters/main.ts index 8062294..6c2fa95 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -26,7 +26,10 @@ import { convert as convertVcf, properties as propertiesVcf } from "./vcf"; import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex"; import { convert as convertMarkitdown, properties as propertiesMarkitdown } from "./markitdown"; import { convert as convertMineru, properties as propertiesMineru } from "./mineru"; -import { convert as convertPDFMathTranslate, properties as propertiesPDFMathTranslate } from "./pdfmathtranslate"; +import { + convert as convertPDFMathTranslate, + properties as propertiesPDFMathTranslate, +} from "./pdfmathtranslate"; // This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular diff --git a/src/converters/mineru.ts b/src/converters/mineru.ts index 1438c0e..db6e929 100644 --- a/src/converters/mineru.ts +++ b/src/converters/mineru.ts @@ -2,7 +2,7 @@ import { execFile as execFileOriginal } from "node:child_process"; import { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync } from "node:fs"; import { join, basename, dirname } from "node:path"; import { ExecFileFn } from "./types"; -import { createConverterArchive, getArchiveFileName } from "../transfer"; +import { getArchiveFileName } from "../transfer"; export const properties = { from: { @@ -16,7 +16,7 @@ export const properties = { /** * Helper function to create a .tar archive from a directory (no compression) - * + * * ⚠️ 重要:僅使用 .tar 格式,禁止 .tar.gz / .tgz / .zip */ function createTarArchive( @@ -28,23 +28,19 @@ function createTarArchive( // Use tar command to create archive (without gzip compression) // tar -cf -C . // 注意:使用 -cf 而非 -czf,避免 gzip 壓縮 - execFile( - "tar", - ["-cf", outputTar, "-C", sourceDir, "."], - (error, stdout, stderr) => { - if (error) { - reject(`tar error: ${error}`); - return; - } - if (stdout) { - console.log(`tar stdout: ${stdout}`); - } - if (stderr) { - console.error(`tar stderr: ${stderr}`); - } - resolve(); - }, - ); + execFile("tar", ["-cf", outputTar, "-C", sourceDir, "."], (error, stdout, stderr) => { + if (error) { + reject(`tar error: ${error}`); + return; + } + if (stdout) { + console.log(`tar stdout: ${stdout}`); + } + if (stderr) { + console.error(`tar stderr: ${stderr}`); + } + resolve(); + }); }); } @@ -75,11 +71,6 @@ export async function convert( execFile: ExecFileFn = execFileOriginal, ): Promise { return new Promise((resolve, reject) => { - // Determine mode based on target format - // md-t = table mode (tables as markdown) - // md-i = image mode (tables as images) - const tableMode = convertTo === "md-t" ? "latex" : "html"; - // Create a temporary output directory for MinerU const outputDir = dirname(targetPath); const inputFileName = basename(filePath, `.${fileType}`); @@ -92,14 +83,7 @@ export async function convert( // Build MinerU command arguments // MinerU CLI: magic-pdf -p -o -m auto - const args = [ - "-p", - filePath, - "-o", - mineruOutputDir, - "-m", - "auto", - ]; + const args = ["-p", filePath, "-o", mineruOutputDir, "-m", "auto"]; // Add table mode option if md-i (render tables as images) if (convertTo === "md-i") { diff --git a/src/converters/pdfmathtranslate.ts b/src/converters/pdfmathtranslate.ts index 57ac3c8..db55cb4 100644 --- a/src/converters/pdfmathtranslate.ts +++ b/src/converters/pdfmathtranslate.ts @@ -6,46 +6,47 @@ import { getArchiveFileName } from "../transfer"; /** * PDFMathTranslate Content Engine - * + * * 用於翻譯 PDF 文件同時保留數學公式的引擎。 - * + * * 使用 pdf2zh CLI 工具進行翻譯,支援多種目標語言。 * 所有輸出一律打包為 .tar 檔案,包含: * - original.pdf(原始 PDF) * - translated-.pdf(翻譯後的 PDF) - * + * * 必須在 Docker build 階段預先下載所需模型, * 不允許在 runtime 隱式下載模型。 */ // 支援的目標語言列表 const SUPPORTED_LANGUAGES = [ - "en", // English - "zh", // Chinese (Simplified) + "en", // English + "zh", // Chinese (Simplified) "zh-TW", // Chinese (Traditional) - "ja", // Japanese - "ko", // Korean - "de", // German - "fr", // French - "es", // Spanish - "it", // Italian - "pt", // Portuguese - "ru", // Russian - "ar", // Arabic - "hi", // Hindi - "vi", // Vietnamese - "th", // Thai + "ja", // Japanese + "ko", // Korean + "de", // German + "fr", // French + "es", // Spanish + "it", // Italian + "pt", // Portuguese + "ru", // Russian + "ar", // Arabic + "hi", // Hindi + "vi", // Vietnamese + "th", // Thai ] as const; -type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number]; - // 模型路徑(Docker 環境中) const MODELS_PATH = process.env.PDFMATHTRANSLATE_MODELS_PATH || "/models/pdfmathtranslate"; // 生成 from/to 格式映射 -function generateLanguageMappings(): { from: Record; to: Record } { - const outputFormats = SUPPORTED_LANGUAGES.map(lang => `pdf-${lang}`); - +function generateLanguageMappings(): { + from: Record; + to: Record; +} { + const outputFormats = SUPPORTED_LANGUAGES.map((lang) => `pdf-${lang}`); + return { from: { document: ["pdf"], @@ -91,7 +92,7 @@ function checkModelsExist(): boolean { /** * Helper function to create a .tar archive from a directory (no compression) - * + * * ⚠️ 重要:僅使用 .tar 格式,禁止 .tar.gz / .tgz / .zip */ function createTarArchive( @@ -103,23 +104,19 @@ function createTarArchive( // Use tar command to create archive (without gzip compression) // tar -cf -C . // 注意:使用 -cf 而非 -czf,避免 gzip 壓縮 - execFile( - "tar", - ["-cf", outputTar, "-C", sourceDir, "."], - (error, stdout, stderr) => { - if (error) { - reject(`tar error: ${error}`); - return; - } - if (stdout) { - console.log(`tar stdout: ${stdout}`); - } - if (stderr) { - console.error(`tar stderr: ${stderr}`); - } - resolve(); - }, - ); + execFile("tar", ["-cf", outputTar, "-C", sourceDir, "."], (error, stdout, stderr) => { + if (error) { + reject(`tar error: ${error}`); + return; + } + if (stdout) { + console.log(`tar stdout: ${stdout}`); + } + if (stderr) { + console.error(`tar stderr: ${stderr}`); + } + resolve(); + }); }); } @@ -143,7 +140,7 @@ function removeDir(dirPath: string): void { /** * 執行 pdf2zh 命令進行 PDF 翻譯 - * + * * @param inputPath 輸入 PDF 路徑 * @param outputDir 輸出目錄 * @param targetLang 目標語言 @@ -160,16 +157,19 @@ function runPdf2zh( // -lo : 目標語言 // -o : 輸出目錄 // -s google: 使用 Google 翻譯(預設,免費) - // + // // 輸出檔案: // - -mono.pdf: 純翻譯版本 // - -dual.pdf: 雙語對照版本 - + const args = [ inputPath, - "-lo", targetLang, - "-o", outputDir, - "-s", process.env.PDFMATHTRANSLATE_SERVICE || "google", + "-lo", + targetLang, + "-o", + outputDir, + "-s", + process.env.PDFMATHTRANSLATE_SERVICE || "google", ]; // 如果設定了自訂模型路徑,使用 --onnx 參數 @@ -205,9 +205,9 @@ function runPdf2zh( if (!existsSync(monoPath) && !existsSync(dualPath)) { // 嘗試在 outputDir 下尋找任何 PDF 檔案 const files = readdirSync(outputDir); - const pdfFiles = files.filter(f => f.endsWith(".pdf")); + const pdfFiles = files.filter((f) => f.endsWith(".pdf")); console.log(`[PDFMathTranslate] Found PDF files: ${pdfFiles.join(", ")}`); - + if (pdfFiles.length === 0) { reject(`No output PDF files found in ${outputDir}`); return; @@ -221,7 +221,7 @@ function runPdf2zh( /** * 主要轉換函數 - * + * * @param filePath 輸入 PDF 檔案路徑 * @param fileType 檔案類型(應為 "pdf") * @param convertTo 目標格式(如 "pdf-zh") @@ -234,93 +234,91 @@ export async function convert( fileType: string, convertTo: string, targetPath: string, - options?: unknown, + _options?: unknown, execFile: ExecFileFn = execFileOriginal, ): Promise { - return new Promise(async (resolve, reject) => { - try { - // 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載) - checkModelsExist(); + try { + // 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載) + checkModelsExist(); - // 2. 提取目標語言 - const targetLang = extractTargetLanguage(convertTo); - console.log(`[PDFMathTranslate] Translating to: ${targetLang}`); + // 2. 提取目標語言 + const targetLang = extractTargetLanguage(convertTo); + console.log(`[PDFMathTranslate] Translating to: ${targetLang}`); - // 3. 建立臨時輸出目錄 - const outputDir = dirname(targetPath); - const inputFileName = basename(filePath, `.${fileType}`); - const tempDir = join(outputDir, `${inputFileName}_pdfmathtranslate_${Date.now()}`); + // 3. 建立臨時輸出目錄 + const outputDir = dirname(targetPath); + const inputFileName = basename(filePath, `.${fileType}`); + const tempDir = join(outputDir, `${inputFileName}_pdfmathtranslate_${Date.now()}`); - if (!existsSync(tempDir)) { - mkdirSync(tempDir, { recursive: true }); - } - - // 4. 建立封裝用目錄 - const archiveDir = join(tempDir, "archive"); - mkdirSync(archiveDir, { recursive: true }); - - // 5. 執行 pdf2zh 翻譯 - const { monoPath, dualPath } = await runPdf2zh(filePath, tempDir, targetLang, execFile); - - // 6. 複製原始檔案到封裝目錄 - const originalDest = join(archiveDir, "original.pdf"); - copyFileSync(filePath, originalDest); - - // 7. 複製翻譯後的檔案(優先使用 mono,因為它是純翻譯版本) - const translatedDest = join(archiveDir, `translated-${targetLang}.pdf`); - - // 檢查各種可能的輸出檔案名稱 - const possibleOutputs = [ - monoPath, - dualPath, - join(tempDir, `${inputFileName}-mono.pdf`), - join(tempDir, `${inputFileName}-dual.pdf`), - ]; - - let foundOutput = false; - for (const outputPath of possibleOutputs) { - if (existsSync(outputPath)) { - copyFileSync(outputPath, translatedDest); - foundOutput = true; - console.log(`[PDFMathTranslate] Using output: ${outputPath}`); - break; - } - } - - // 如果找不到預期的輸出檔案,嘗試找任何 PDF - if (!foundOutput) { - const allFiles = readdirSync(tempDir); - const pdfFiles = allFiles.filter(f => f.endsWith(".pdf") && f !== basename(filePath)); - const firstPdfName = pdfFiles[0]; - - if (firstPdfName) { - const firstPdf = join(tempDir, firstPdfName); - copyFileSync(firstPdf, translatedDest); - foundOutput = true; - console.log(`[PDFMathTranslate] Using fallback output: ${firstPdf}`); - } - } - - if (!foundOutput) { - throw new Error("No translated PDF output found"); - } - - // 8. 建立 .tar 封裝 - const tarPath = getArchiveFileName(targetPath); - const tarDir = dirname(tarPath); - if (!existsSync(tarDir)) { - mkdirSync(tarDir, { recursive: true }); - } - - await createTarArchive(archiveDir, tarPath, execFile); - console.log(`[PDFMathTranslate] Created archive: ${tarPath}`); - - // 9. 清理臨時目錄 - removeDir(tempDir); - - resolve("Done"); - } catch (error) { - reject(`PDFMathTranslate error: ${error}`); + if (!existsSync(tempDir)) { + mkdirSync(tempDir, { recursive: true }); } - }); + + // 4. 建立封裝用目錄 + const archiveDir = join(tempDir, "archive"); + mkdirSync(archiveDir, { recursive: true }); + + // 5. 執行 pdf2zh 翻譯 + const { monoPath, dualPath } = await runPdf2zh(filePath, tempDir, targetLang, execFile); + + // 6. 複製原始檔案到封裝目錄 + const originalDest = join(archiveDir, "original.pdf"); + copyFileSync(filePath, originalDest); + + // 7. 複製翻譯後的檔案(優先使用 mono,因為它是純翻譯版本) + const translatedDest = join(archiveDir, `translated-${targetLang}.pdf`); + + // 檢查各種可能的輸出檔案名稱 + const possibleOutputs = [ + monoPath, + dualPath, + join(tempDir, `${inputFileName}-mono.pdf`), + join(tempDir, `${inputFileName}-dual.pdf`), + ]; + + let foundOutput = false; + for (const outputPath of possibleOutputs) { + if (existsSync(outputPath)) { + copyFileSync(outputPath, translatedDest); + foundOutput = true; + console.log(`[PDFMathTranslate] Using output: ${outputPath}`); + break; + } + } + + // 如果找不到預期的輸出檔案,嘗試找任何 PDF + if (!foundOutput) { + const allFiles = readdirSync(tempDir); + const pdfFiles = allFiles.filter((f) => f.endsWith(".pdf") && f !== basename(filePath)); + const firstPdfName = pdfFiles[0]; + + if (firstPdfName) { + const firstPdf = join(tempDir, firstPdfName); + copyFileSync(firstPdf, translatedDest); + foundOutput = true; + console.log(`[PDFMathTranslate] Using fallback output: ${firstPdf}`); + } + } + + if (!foundOutput) { + throw new Error("No translated PDF output found"); + } + + // 8. 建立 .tar 封裝 + const tarPath = getArchiveFileName(targetPath); + const tarDir = dirname(tarPath); + if (!existsSync(tarDir)) { + mkdirSync(tarDir, { recursive: true }); + } + + await createTarArchive(archiveDir, tarPath, execFile); + console.log(`[PDFMathTranslate] Created archive: ${tarPath}`); + + // 9. 清理臨時目錄 + removeDir(tempDir); + + return "Done"; + } catch (error) { + throw new Error(`PDFMathTranslate error: ${error}`); + } } diff --git a/src/pages/download.tsx b/src/pages/download.tsx index 9701957..55c2ac3 100644 --- a/src/pages/download.tsx +++ b/src/pages/download.tsx @@ -1,11 +1,10 @@ -import path from "node:path"; import { Elysia } from "elysia"; import sanitize from "sanitize-filename"; import { outputDir } from ".."; import db from "../db/db"; import { WEBROOT } from "../helpers/env"; import { userService } from "./user"; -import { createJobArchive, shouldUseChunkedDownload } from "../transfer"; +import { createJobArchive } from "../transfer"; export const download = new Elysia() .use(userService) @@ -45,10 +44,10 @@ export const download = new Elysia() const jobId = decodeURIComponent(params.jobId); const outputPath = `${outputDir}${userId}/${jobId}`; - + // 使用統一的封裝管理器建立 .tar(不壓縮) const outputTar = await createJobArchive(outputPath, jobId); - + return Bun.file(outputTar); }, { diff --git a/src/pages/downloadChunk.tsx b/src/pages/downloadChunk.tsx index 8ccc358..507c5e8 100644 --- a/src/pages/downloadChunk.tsx +++ b/src/pages/downloadChunk.tsx @@ -1,14 +1,12 @@ /** * Contents.CN Chunk 下載 API - * + * * 處理大檔的分段下載 */ -import { Elysia, t } from "elysia"; -import { join } from "node:path"; +import { Elysia } from "elysia"; import { outputDir } from ".."; import db from "../db/db"; -import { WEBROOT } from "../helpers/env"; import { userService } from "./user"; import sanitize from "sanitize-filename"; import { @@ -16,11 +14,8 @@ import { getChunkDownloadInfo, getChunk, createChunkDownloadHeaders, - getFileForDirectDownload, - createDirectDownloadHeaders, - CHUNK_SIZE_BYTES, } from "../transfer"; -import { statSync, existsSync } from "node:fs"; +import { existsSync } from "node:fs"; export const downloadChunk = new Elysia() .use(userService) @@ -44,7 +39,7 @@ export const downloadChunk = new Elysia() const filePath = `${outputDir}${userId}/${jobId}/${fileName}`; const info = getChunkDownloadInfo(filePath); - + if (!info) { return { error: "File not found" }; } @@ -54,7 +49,7 @@ export const downloadChunk = new Elysia() use_chunked: shouldUseChunkedDownload(filePath), }; }, - { auth: true } + { auth: true }, ) /** * 下載特定 chunk @@ -90,14 +85,14 @@ export const downloadChunk = new Elysia() } const headers = createChunkDownloadHeaders(info, chunkIndex, chunkData); - + for (const [key, value] of Object.entries(headers)) { set.headers[key] = value; } return new Response(chunkData); }, - { auth: true } + { auth: true }, ) /** * Archive chunk 下載資訊 @@ -122,7 +117,7 @@ export const downloadChunk = new Elysia() } const info = getChunkDownloadInfo(archivePath); - + if (!info) { return { error: "Archive not found" }; } @@ -132,7 +127,7 @@ export const downloadChunk = new Elysia() use_chunked: shouldUseChunkedDownload(archivePath), }; }, - { auth: true } + { auth: true }, ) /** * Archive chunk 下載 @@ -167,12 +162,12 @@ export const downloadChunk = new Elysia() } const headers = createChunkDownloadHeaders(info, chunkIndex, chunkData); - + for (const [key, value] of Object.entries(headers)) { set.headers[key] = value; } return new Response(chunkData); }, - { auth: true } + { auth: true }, ); diff --git a/src/pages/uploadChunk.tsx b/src/pages/uploadChunk.tsx index ad0dc3f..9c2652b 100644 --- a/src/pages/uploadChunk.tsx +++ b/src/pages/uploadChunk.tsx @@ -1,11 +1,10 @@ /** * Contents.CN Chunk 上傳 API - * + * * 處理大檔的分段上傳 */ import { Elysia, t } from "elysia"; -import { WEBROOT } from "../helpers/env"; import { uploadsDir } from "../index"; import { userService } from "./user"; import sanitize from "sanitize-filename"; @@ -44,7 +43,7 @@ export const uploadChunk = new Elysia().use(userService).post( user.id, jobId.value, `${uploadsDir}${user.id}/`, - userUploadsDir + userUploadsDir, ); return result; @@ -59,7 +58,7 @@ export const uploadChunk = new Elysia().use(userService).post( chunk: t.File(), }), auth: true, - } + }, ); /** @@ -72,7 +71,7 @@ export const uploadInfo = new Elysia().use(userService).post( const size = parseInt(file_size, 10); const useChunked = shouldUseChunkedUpload(size); - + return { use_chunked: useChunked, chunk_size: CHUNK_SIZE_BYTES, @@ -84,5 +83,5 @@ export const uploadInfo = new Elysia().use(userService).post( file_size: t.String(), }), auth: true, - } + }, ); diff --git a/src/pages/user.tsx b/src/pages/user.tsx index 5567c78..4b96351 100644 --- a/src/pages/user.tsx +++ b/src/pages/user.tsx @@ -115,12 +115,16 @@ export const user = new Elysia() sm:px-4 `} > -

{t("setup", "welcome")}

+

+ {t("setup", "welcome")} +

-
{t("setup", "createYourAccount")}
+
+ {t("setup", "createYourAccount")} +
-