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
This commit is contained in:
Your Name 2026-01-21 16:00:26 +08:00
parent db5f47586e
commit 79fc6f7067
21 changed files with 439 additions and 439 deletions

View file

@ -1,6 +1,5 @@
const webroot = document.querySelector("meta[name='webroot']").content; const webroot = document.querySelector("meta[name='webroot']").content;
const fileInput = document.querySelector('input[type="file"]'); const fileInput = document.querySelector('input[type="file"]');
const dropZone = document.getElementById("dropzone");
const convertButton = document.querySelector("input[type='submit']"); const convertButton = document.querySelector("input[type='submit']");
const fileNames = []; const fileNames = [];
let fileType; let fileType;

View file

@ -72,7 +72,7 @@
applyTheme(preferredTheme); applyTheme(preferredTheme);
// Listen for system preference changes // 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 // Only update if no manual preference is set
if (!localStorage.getItem(THEME_KEY)) { if (!localStorage.getItem(THEME_KEY)) {
applyTheme(null); applyTheme(null);

View file

@ -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 = ({ export const ThemeToggle = ({
locale = defaultLocale,
t = createTranslator(defaultLocale), t = createTranslator(defaultLocale),
}: { }: {
locale?: SupportedLocale; locale?: SupportedLocale;

View file

@ -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 convertxelatex, properties as propertiesxelatex } from "./xelatex";
import { convert as convertMarkitdown, properties as propertiesMarkitdown } from "./markitdown"; import { convert as convertMarkitdown, properties as propertiesMarkitdown } from "./markitdown";
import { convert as convertMineru, properties as propertiesMineru } from "./mineru"; 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 // This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular

View file

@ -2,7 +2,7 @@ import { execFile as execFileOriginal } from "node:child_process";
import { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync } from "node:fs"; import { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync } from "node:fs";
import { join, basename, dirname } from "node:path"; import { join, basename, dirname } from "node:path";
import { ExecFileFn } from "./types"; import { ExecFileFn } from "./types";
import { createConverterArchive, getArchiveFileName } from "../transfer"; import { getArchiveFileName } from "../transfer";
export const properties = { export const properties = {
from: { from: {
@ -28,23 +28,19 @@ function createTarArchive(
// Use tar command to create archive (without gzip compression) // Use tar command to create archive (without gzip compression)
// tar -cf <output.tar> -C <sourceDir> . // tar -cf <output.tar> -C <sourceDir> .
// 注意:使用 -cf 而非 -czf避免 gzip 壓縮 // 注意:使用 -cf 而非 -czf避免 gzip 壓縮
execFile( execFile("tar", ["-cf", outputTar, "-C", sourceDir, "."], (error, stdout, stderr) => {
"tar", if (error) {
["-cf", outputTar, "-C", sourceDir, "."], reject(`tar error: ${error}`);
(error, stdout, stderr) => { return;
if (error) { }
reject(`tar error: ${error}`); if (stdout) {
return; console.log(`tar stdout: ${stdout}`);
} }
if (stdout) { if (stderr) {
console.log(`tar stdout: ${stdout}`); console.error(`tar stderr: ${stderr}`);
} }
if (stderr) { resolve();
console.error(`tar stderr: ${stderr}`); });
}
resolve();
},
);
}); });
} }
@ -75,11 +71,6 @@ export async function convert(
execFile: ExecFileFn = execFileOriginal, execFile: ExecFileFn = execFileOriginal,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { 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 // Create a temporary output directory for MinerU
const outputDir = dirname(targetPath); const outputDir = dirname(targetPath);
const inputFileName = basename(filePath, `.${fileType}`); const inputFileName = basename(filePath, `.${fileType}`);
@ -92,14 +83,7 @@ export async function convert(
// Build MinerU command arguments // Build MinerU command arguments
// MinerU CLI: magic-pdf -p <input> -o <output_dir> -m auto // MinerU CLI: magic-pdf -p <input> -o <output_dir> -m auto
const args = [ const args = ["-p", filePath, "-o", mineruOutputDir, "-m", "auto"];
"-p",
filePath,
"-o",
mineruOutputDir,
"-m",
"auto",
];
// Add table mode option if md-i (render tables as images) // Add table mode option if md-i (render tables as images)
if (convertTo === "md-i") { if (convertTo === "md-i") {

View file

@ -20,31 +20,32 @@ import { getArchiveFileName } from "../transfer";
// 支援的目標語言列表 // 支援的目標語言列表
const SUPPORTED_LANGUAGES = [ const SUPPORTED_LANGUAGES = [
"en", // English "en", // English
"zh", // Chinese (Simplified) "zh", // Chinese (Simplified)
"zh-TW", // Chinese (Traditional) "zh-TW", // Chinese (Traditional)
"ja", // Japanese "ja", // Japanese
"ko", // Korean "ko", // Korean
"de", // German "de", // German
"fr", // French "fr", // French
"es", // Spanish "es", // Spanish
"it", // Italian "it", // Italian
"pt", // Portuguese "pt", // Portuguese
"ru", // Russian "ru", // Russian
"ar", // Arabic "ar", // Arabic
"hi", // Hindi "hi", // Hindi
"vi", // Vietnamese "vi", // Vietnamese
"th", // Thai "th", // Thai
] as const; ] as const;
type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
// 模型路徑Docker 環境中) // 模型路徑Docker 環境中)
const MODELS_PATH = process.env.PDFMATHTRANSLATE_MODELS_PATH || "/models/pdfmathtranslate"; const MODELS_PATH = process.env.PDFMATHTRANSLATE_MODELS_PATH || "/models/pdfmathtranslate";
// 生成 from/to 格式映射 // 生成 from/to 格式映射
function generateLanguageMappings(): { from: Record<string, string[]>; to: Record<string, string[]> } { function generateLanguageMappings(): {
const outputFormats = SUPPORTED_LANGUAGES.map(lang => `pdf-${lang}`); from: Record<string, string[]>;
to: Record<string, string[]>;
} {
const outputFormats = SUPPORTED_LANGUAGES.map((lang) => `pdf-${lang}`);
return { return {
from: { from: {
@ -103,23 +104,19 @@ function createTarArchive(
// Use tar command to create archive (without gzip compression) // Use tar command to create archive (without gzip compression)
// tar -cf <output.tar> -C <sourceDir> . // tar -cf <output.tar> -C <sourceDir> .
// 注意:使用 -cf 而非 -czf避免 gzip 壓縮 // 注意:使用 -cf 而非 -czf避免 gzip 壓縮
execFile( execFile("tar", ["-cf", outputTar, "-C", sourceDir, "."], (error, stdout, stderr) => {
"tar", if (error) {
["-cf", outputTar, "-C", sourceDir, "."], reject(`tar error: ${error}`);
(error, stdout, stderr) => { return;
if (error) { }
reject(`tar error: ${error}`); if (stdout) {
return; console.log(`tar stdout: ${stdout}`);
} }
if (stdout) { if (stderr) {
console.log(`tar stdout: ${stdout}`); console.error(`tar stderr: ${stderr}`);
} }
if (stderr) { resolve();
console.error(`tar stderr: ${stderr}`); });
}
resolve();
},
);
}); });
} }
@ -167,9 +164,12 @@ function runPdf2zh(
const args = [ const args = [
inputPath, inputPath,
"-lo", targetLang, "-lo",
"-o", outputDir, targetLang,
"-s", process.env.PDFMATHTRANSLATE_SERVICE || "google", "-o",
outputDir,
"-s",
process.env.PDFMATHTRANSLATE_SERVICE || "google",
]; ];
// 如果設定了自訂模型路徑,使用 --onnx 參數 // 如果設定了自訂模型路徑,使用 --onnx 參數
@ -205,7 +205,7 @@ function runPdf2zh(
if (!existsSync(monoPath) && !existsSync(dualPath)) { if (!existsSync(monoPath) && !existsSync(dualPath)) {
// 嘗試在 outputDir 下尋找任何 PDF 檔案 // 嘗試在 outputDir 下尋找任何 PDF 檔案
const files = readdirSync(outputDir); 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(", ")}`); console.log(`[PDFMathTranslate] Found PDF files: ${pdfFiles.join(", ")}`);
if (pdfFiles.length === 0) { if (pdfFiles.length === 0) {
@ -234,93 +234,91 @@ export async function convert(
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
options?: unknown, _options?: unknown,
execFile: ExecFileFn = execFileOriginal, execFile: ExecFileFn = execFileOriginal,
): Promise<string> { ): Promise<string> {
return new Promise(async (resolve, reject) => { try {
try { // 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載)
// 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載) checkModelsExist();
checkModelsExist();
// 2. 提取目標語言 // 2. 提取目標語言
const targetLang = extractTargetLanguage(convertTo); const targetLang = extractTargetLanguage(convertTo);
console.log(`[PDFMathTranslate] Translating to: ${targetLang}`); console.log(`[PDFMathTranslate] Translating to: ${targetLang}`);
// 3. 建立臨時輸出目錄 // 3. 建立臨時輸出目錄
const outputDir = dirname(targetPath); const outputDir = dirname(targetPath);
const inputFileName = basename(filePath, `.${fileType}`); const inputFileName = basename(filePath, `.${fileType}`);
const tempDir = join(outputDir, `${inputFileName}_pdfmathtranslate_${Date.now()}`); const tempDir = join(outputDir, `${inputFileName}_pdfmathtranslate_${Date.now()}`);
if (!existsSync(tempDir)) { if (!existsSync(tempDir)) {
mkdirSync(tempDir, { recursive: true }); 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}`);
} }
});
// 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}`);
}
} }

View file

@ -1,11 +1,10 @@
import path from "node:path";
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
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 { createJobArchive, shouldUseChunkedDownload } from "../transfer"; import { createJobArchive } from "../transfer";
export const download = new Elysia() export const download = new Elysia()
.use(userService) .use(userService)

View file

@ -4,11 +4,9 @@
* *
*/ */
import { Elysia, t } from "elysia"; import { Elysia } from "elysia";
import { join } from "node:path";
import { outputDir } from ".."; import { outputDir } from "..";
import db from "../db/db"; import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { userService } from "./user"; import { userService } from "./user";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import { import {
@ -16,11 +14,8 @@ import {
getChunkDownloadInfo, getChunkDownloadInfo,
getChunk, getChunk,
createChunkDownloadHeaders, createChunkDownloadHeaders,
getFileForDirectDownload,
createDirectDownloadHeaders,
CHUNK_SIZE_BYTES,
} from "../transfer"; } from "../transfer";
import { statSync, existsSync } from "node:fs"; import { existsSync } from "node:fs";
export const downloadChunk = new Elysia() export const downloadChunk = new Elysia()
.use(userService) .use(userService)
@ -54,7 +49,7 @@ export const downloadChunk = new Elysia()
use_chunked: shouldUseChunkedDownload(filePath), use_chunked: shouldUseChunkedDownload(filePath),
}; };
}, },
{ auth: true } { auth: true },
) )
/** /**
* chunk * chunk
@ -97,7 +92,7 @@ export const downloadChunk = new Elysia()
return new Response(chunkData); return new Response(chunkData);
}, },
{ auth: true } { auth: true },
) )
/** /**
* Archive chunk * Archive chunk
@ -132,7 +127,7 @@ export const downloadChunk = new Elysia()
use_chunked: shouldUseChunkedDownload(archivePath), use_chunked: shouldUseChunkedDownload(archivePath),
}; };
}, },
{ auth: true } { auth: true },
) )
/** /**
* Archive chunk * Archive chunk
@ -174,5 +169,5 @@ export const downloadChunk = new Elysia()
return new Response(chunkData); return new Response(chunkData);
}, },
{ auth: true } { auth: true },
); );

View file

@ -5,7 +5,6 @@
*/ */
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { WEBROOT } from "../helpers/env";
import { uploadsDir } from "../index"; import { uploadsDir } from "../index";
import { userService } from "./user"; import { userService } from "./user";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
@ -44,7 +43,7 @@ export const uploadChunk = new Elysia().use(userService).post(
user.id, user.id,
jobId.value, jobId.value,
`${uploadsDir}${user.id}/`, `${uploadsDir}${user.id}/`,
userUploadsDir userUploadsDir,
); );
return result; return result;
@ -59,7 +58,7 @@ export const uploadChunk = new Elysia().use(userService).post(
chunk: t.File(), chunk: t.File(),
}), }),
auth: true, auth: true,
} },
); );
/** /**
@ -84,5 +83,5 @@ export const uploadInfo = new Elysia().use(userService).post(
file_size: t.String(), file_size: t.String(),
}), }),
auth: true, auth: true,
} },
); );

View file

@ -115,12 +115,16 @@ export const user = new Elysia()
sm:px-4 sm:px-4
`} `}
> >
<h1 class="my-8 text-3xl">{t("setup", "welcome")}</h1> <h1 class="my-8 text-3xl" safe>
{t("setup", "welcome")}
</h1>
<article class="article p-0"> <article class="article p-0">
<header class="w-full bg-neutral-800 p-4">{t("setup", "createYourAccount")}</header> <header class="w-full bg-neutral-800 p-4" safe>
{t("setup", "createYourAccount")}
</header>
<form method="post" action={`${WEBROOT}/register`} class="p-4"> <form method="post" action={`${WEBROOT}/register`} class="p-4">
<fieldset class="mb-4 flex flex-col gap-4"> <fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "email")} {t("auth", "email")}
<input <input
type="email" type="email"
@ -131,7 +135,7 @@ export const user = new Elysia()
required required
/> />
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "password")} {t("auth", "password")}
<input <input
type="password" type="password"
@ -146,13 +150,14 @@ export const user = new Elysia()
<input type="submit" value={t("auth", "createAccount")} class="btn-primary" /> <input type="submit" value={t("auth", "createAccount")} class="btn-primary" />
</form> </form>
<footer class="p-4"> <footer class="p-4">
{t("setup", "reportIssues")}{" "} <span safe>{t("setup", "reportIssues")}</span>{" "}
<a <a
class={` class={`
text-accent-500 underline text-accent-500 underline
hover:text-accent-400 hover:text-accent-400
`} `}
href="https://github.com/pi-docket/ConvertX-CN" href="https://github.com/pi-docket/ConvertX-CN"
safe
> >
{t("setup", "github")} {t("setup", "github")}
</a> </a>
@ -186,7 +191,7 @@ export const user = new Elysia()
<article class="article"> <article class="article">
<form method="post" class="flex flex-col gap-4"> <form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4"> <fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "email")} {t("auth", "email")}
<input <input
type="email" type="email"
@ -197,7 +202,7 @@ export const user = new Elysia()
required required
/> />
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "password")} {t("auth", "password")}
<input <input
type="password" type="password"
@ -308,7 +313,7 @@ export const user = new Elysia()
<article class="article"> <article class="article">
<form method="post" class="flex flex-col gap-4"> <form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4"> <fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "email")} {t("auth", "email")}
<input <input
type="email" type="email"
@ -319,7 +324,7 @@ export const user = new Elysia()
required required
/> />
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "password")} {t("auth", "password")}
<input <input
type="password" type="password"
@ -336,6 +341,7 @@ export const user = new Elysia()
href={`${WEBROOT}/register`} href={`${WEBROOT}/register`}
role="button" role="button"
class="w-full btn-secondary text-center" class="w-full btn-secondary text-center"
safe
> >
{t("nav", "register")} {t("nav", "register")}
</a> </a>
@ -444,7 +450,7 @@ export const user = new Elysia()
<article class="article"> <article class="article">
<form method="post" class="flex flex-col gap-4"> <form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4"> <fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "email")} {t("auth", "email")}
<input <input
type="email" type="email"
@ -456,7 +462,7 @@ export const user = new Elysia()
required required
/> />
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "newPassword")} {t("auth", "newPassword")}
<input <input
type="password" type="password"
@ -466,7 +472,7 @@ export const user = new Elysia()
autocomplete="new-password" autocomplete="new-password"
/> />
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1" safe>
{t("auth", "currentPassword")} {t("auth", "currentPassword")}
<input <input
type="password" type="password"

View file

@ -6,8 +6,8 @@
* - .tar.gz, .tgz, .zip * - .tar.gz, .tgz, .zip
*/ */
import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs"; import { existsSync, mkdirSync, readdirSync } from "node:fs";
import { join, basename } from "node:path"; import { join } from "node:path";
import * as tar from "tar"; import * as tar from "tar";
import { ALLOWED_ARCHIVE_FORMAT, FORBIDDEN_ARCHIVE_FORMATS } from "./constants"; import { ALLOWED_ARCHIVE_FORMAT, FORBIDDEN_ARCHIVE_FORMATS } from "./constants";
@ -65,7 +65,7 @@ export async function createTarArchive(
options: { options: {
filter?: (path: string) => boolean; filter?: (path: string) => boolean;
prefix?: string; prefix?: string;
} = {} } = {},
): Promise<string> { ): Promise<string> {
// 強制確保輸出是 .tar 格式 // 強制確保輸出是 .tar 格式
const finalOutputPath = getArchiveFileName(outputPath); const finalOutputPath = getArchiveFileName(outputPath);
@ -91,7 +91,7 @@ export async function createTarArchive(
// 不使用 gzip 壓縮 // 不使用 gzip 壓縮
gzip: false, gzip: false,
}, },
files.filter(f => filter(f)) files.filter((f) => filter(f)),
); );
console.log(`Created tar archive: ${finalOutputPath}`); console.log(`Created tar archive: ${finalOutputPath}`);
@ -114,41 +114,3 @@ export async function createJobArchive(outputDir: string, jobId: string): Promis
return archivePath; return archivePath;
} }
/**
* MinerU
*
* @param mineruOutputDir - MinerU
* @param targetPath - .tar
*/
export async function createConverterArchive(
converterOutputDir: string,
targetPath: string
): Promise<string> {
// 確保輸出使用 .tar 格式
const tarPath = getArchiveFileName(targetPath);
await createTarArchive(converterOutputDir, tarPath);
return tarPath;
}
/**
*
*/
export function getArchiveInfo(archivePath: string): {
exists: boolean;
size: number;
fileName: string;
} | null {
if (!existsSync(archivePath)) {
return null;
}
const stats = statSync(archivePath);
return {
exists: true,
size: stats.size,
fileName: basename(archivePath),
};
}

View file

@ -48,7 +48,7 @@ export function getChunkDownloadInfo(filePath: string): ChunkDownloadInfo | null
export async function getChunk( export async function getChunk(
filePath: string, filePath: string,
chunkIndex: number, chunkIndex: number,
chunkSize: number = CHUNK_SIZE_BYTES chunkSize: number = CHUNK_SIZE_BYTES,
): Promise<Buffer | null> { ): Promise<Buffer | null> {
if (!existsSync(filePath)) { if (!existsSync(filePath)) {
return null; return null;
@ -78,23 +78,13 @@ export async function getChunk(
}); });
} }
/**
*
*/
export function getFileForDirectDownload(filePath: string): ReturnType<typeof Bun.file> | null {
if (!existsSync(filePath)) {
return null;
}
return Bun.file(filePath);
}
/** /**
* chunk headers * chunk headers
*/ */
export function createChunkDownloadHeaders( export function createChunkDownloadHeaders(
info: ChunkDownloadInfo, info: ChunkDownloadInfo,
chunkIndex: number, chunkIndex: number,
chunkData: Buffer chunkData: Buffer,
): Record<string, string> { ): Record<string, string> {
const start = chunkIndex * info.chunk_size; const start = chunkIndex * info.chunk_size;
const end = start + chunkData.length - 1; const end = start + chunkData.length - 1;
@ -110,14 +100,3 @@ export function createChunkDownloadHeaders(
"X-Total-Size": info.total_size.toString(), "X-Total-Size": info.total_size.toString(),
}; };
} }
/**
* headers
*/
export function createDirectDownloadHeaders(fileName: string, fileSize: number): Record<string, string> {
return {
"Content-Type": "application/octet-stream",
"Content-Length": fileSize.toString(),
"Content-Disposition": `attachment; filename="${encodeURIComponent(fileName)}"`,
};
}

View file

@ -16,7 +16,6 @@ export {
// 類型 // 類型
export type { export type {
ChunkUploadRequest,
ChunkUploadResponse, ChunkUploadResponse,
DirectUploadResponse, DirectUploadResponse,
ChunkDownloadInfo, ChunkDownloadInfo,
@ -40,9 +39,7 @@ export {
shouldUseChunkedDownload, shouldUseChunkedDownload,
getChunkDownloadInfo, getChunkDownloadInfo,
getChunk, getChunk,
getFileForDirectDownload,
createChunkDownloadHeaders, createChunkDownloadHeaders,
createDirectDownloadHeaders,
} from "./downloadManager"; } from "./downloadManager";
// 封裝管理 // 封裝管理
@ -51,6 +48,4 @@ export {
getArchiveFileName, getArchiveFileName,
createTarArchive, createTarArchive,
createJobArchive, createJobArchive,
createConverterArchive,
getArchiveInfo,
} from "./archiveManager"; } from "./archiveManager";

View file

@ -2,24 +2,6 @@
* Contents.CN * Contents.CN
*/ */
/**
* Chunk
*/
export interface ChunkUploadRequest {
/** 上傳會話 IDUUID */
upload_id: string;
/** 當前 chunk 索引(從 0 開始) */
chunk_index: number;
/** 總 chunk 數量 */
total_chunks: number;
/** chunk 數據 */
data: Blob | ArrayBuffer;
/** 原始檔案名稱 */
file_name: string;
/** 檔案總大小 */
total_size: number;
}
/** /**
* Chunk * Chunk
*/ */

View file

@ -8,7 +8,12 @@
import { existsSync, mkdirSync, rmSync, readdirSync, createWriteStream, statSync } from "node:fs"; import { existsSync, mkdirSync, rmSync, readdirSync, createWriteStream, statSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { CHUNK_THRESHOLD_BYTES, CHUNK_SIZE_BYTES, UPLOAD_SESSION_TIMEOUT_MS, CHUNK_TEMP_DIR } from "./constants"; import {
CHUNK_THRESHOLD_BYTES,
CHUNK_SIZE_BYTES,
UPLOAD_SESSION_TIMEOUT_MS,
CHUNK_TEMP_DIR,
} from "./constants";
import type { UploadSession, ChunkUploadResponse, DirectUploadResponse } from "./types"; import type { UploadSession, ChunkUploadResponse, DirectUploadResponse } from "./types";
import { getTransferMode } from "./types"; import { getTransferMode } from "./types";
@ -22,9 +27,12 @@ class UploadSessionManager {
constructor() { constructor() {
// 定期清理過期的 sessions // 定期清理過期的 sessions
this.cleanupInterval = setInterval(() => { this.cleanupInterval = setInterval(
this.cleanupExpiredSessions(); () => {
}, 5 * 60 * 1000); // 每 5 分鐘清理一次 this.cleanupExpiredSessions();
},
5 * 60 * 1000,
); // 每 5 分鐘清理一次
} }
/** /**
@ -37,7 +45,7 @@ class UploadSessionManager {
fileName: string, fileName: string,
totalSize: number, totalSize: number,
totalChunks: number, totalChunks: number,
baseTempDir: string baseTempDir: string,
): UploadSession { ): UploadSession {
const tempDir = join(baseTempDir, CHUNK_TEMP_DIR, uploadId); const tempDir = join(baseTempDir, CHUNK_TEMP_DIR, uploadId);
@ -140,7 +148,7 @@ export function shouldUseChunkedUpload(fileSize: number): boolean {
export async function handleDirectUpload( export async function handleDirectUpload(
file: File, file: File,
targetDir: string, targetDir: string,
fileName: string fileName: string,
): Promise<DirectUploadResponse> { ): Promise<DirectUploadResponse> {
try { try {
const targetPath = join(targetDir, fileName); const targetPath = join(targetDir, fileName);
@ -178,7 +186,7 @@ export async function handleChunkUpload(
userId: string, userId: string,
jobId: string, jobId: string,
baseTempDir: string, baseTempDir: string,
targetDir: string targetDir: string,
): Promise<ChunkUploadResponse> { ): Promise<ChunkUploadResponse> {
try { try {
// 取得或建立會話 // 取得或建立會話
@ -192,7 +200,7 @@ export async function handleChunkUpload(
fileName, fileName,
totalSize, totalSize,
totalChunks, totalChunks,
baseTempDir baseTempDir,
); );
} }
@ -264,7 +272,7 @@ async function mergeChunks(session: UploadSession, targetDir: string): Promise<s
// 讀取並排序所有 chunks // 讀取並排序所有 chunks
const chunkFiles = readdirSync(session.temp_dir) const chunkFiles = readdirSync(session.temp_dir)
.filter(f => f.startsWith("chunk_")) .filter((f) => f.startsWith("chunk_"))
.sort(); .sort();
// 建立輸出串流 // 建立輸出串流
@ -298,6 +306,9 @@ async function mergeChunks(session: UploadSession, targetDir: string): Promise<s
/** /**
* chunk * chunk
*/ */
export function calculateChunkCount(fileSize: number, chunkSize: number = CHUNK_SIZE_BYTES): number { export function calculateChunkCount(
fileSize: number,
chunkSize: number = CHUNK_SIZE_BYTES,
): number {
return Math.ceil(fileSize / chunkSize); return Math.ceil(fileSize / chunkSize);
} }

View file

@ -51,7 +51,6 @@ describe("MinerU converter md-t mode", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "mineru") { if (cmd === "mineru") {
mineruArgs = args; mineruArgs = args;
@ -64,7 +63,10 @@ describe("MinerU converter md-t mode", () => {
if (!existsSync(autoDir)) { if (!existsSync(autoDir)) {
mkdirSync(autoDir, { recursive: true }); mkdirSync(autoDir, { recursive: true });
} }
writeFileSync(join(autoDir, "output.md"), "# Test\n\n| Col1 | Col2 |\n|---|---|\n| A | B |"); writeFileSync(
join(autoDir, "output.md"),
"# Test\n\n| Col1 | Col2 |\n|---|---|\n| A | B |",
);
callback(null, "MinerU conversion complete", ""); callback(null, "MinerU conversion complete", "");
} else if (cmd === "tar") { } else if (cmd === "tar") {
// Simulate .tar creation (no compression) // Simulate .tar creation (no compression)
@ -103,7 +105,6 @@ describe("MinerU converter md-i mode", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "mineru") { if (cmd === "mineru") {
capturedArgs = args; capturedArgs = args;
@ -156,7 +157,6 @@ describe("MinerU converter .tar output (no compression)", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "mineru") { if (cmd === "mineru") {
// Simulate MinerU creating output // Simulate MinerU creating output
@ -194,9 +194,8 @@ describe("MinerU converter .tar output (no compression)", () => {
test("should reject on mineru error", async () => { test("should reject on mineru error", async () => {
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
cmd: string, cmd: string,
args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "mineru") { if (cmd === "mineru") {
callback(new Error("MinerU failed") as ExecFileException, "", "Error processing file"); callback(new Error("MinerU failed") as ExecFileException, "", "Error processing file");
@ -204,8 +203,9 @@ describe("MinerU converter .tar output (no compression)", () => {
}; };
const targetPath = join(testDir, "output.tar"); const targetPath = join(testDir, "output.tar");
expect(convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile)) expect(convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile)).rejects.toMatch(
.rejects.toMatch(/mineru error/); /mineru error/,
);
}); });
test("should use correct tar arguments without compression", async () => { test("should use correct tar arguments without compression", async () => {
@ -215,7 +215,6 @@ describe("MinerU converter .tar output (no compression)", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "mineru") { if (cmd === "mineru") {
const outputDir = args[3]; const outputDir = args[3];
@ -253,7 +252,6 @@ describe("MinerU converter .tar output (no compression)", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "mineru") { if (cmd === "mineru") {
const outputDir = args[3]; const outputDir = args[3];

View file

@ -2,7 +2,7 @@ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
import { convert, properties } from "../../src/converters/pdfmathtranslate"; import { convert, properties } from "../../src/converters/pdfmathtranslate";
import type { ExecFileException } from "node:child_process"; import type { ExecFileException } from "node:child_process";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { mkdirSync, existsSync, writeFileSync, rmSync, readFileSync } from "node:fs"; import { mkdirSync, existsSync, writeFileSync, rmSync, readdirSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
// Skip common tests as PDFMathTranslate has different behavior (archive output) // Skip common tests as PDFMathTranslate has different behavior (archive output)
@ -54,7 +54,6 @@ describe("PDFMathTranslate converter - Chinese translation", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "pdf2zh") { if (cmd === "pdf2zh") {
pdf2zhCalled = true; pdf2zhCalled = true;
@ -116,7 +115,6 @@ describe("PDFMathTranslate converter - English translation", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "pdf2zh") { if (cmd === "pdf2zh") {
pdf2zhArgs = args; pdf2zhArgs = args;
@ -166,7 +164,6 @@ describe("PDFMathTranslate converter - Japanese translation", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "pdf2zh") { if (cmd === "pdf2zh") {
pdf2zhArgs = args; pdf2zhArgs = args;
@ -216,7 +213,6 @@ describe("PDFMathTranslate converter - Traditional Chinese", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "pdf2zh") { if (cmd === "pdf2zh") {
pdf2zhArgs = args; pdf2zhArgs = args;
@ -267,7 +263,6 @@ describe("PDFMathTranslate converter - Output structure", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "pdf2zh") { if (cmd === "pdf2zh") {
const outputDirIndex = args.indexOf("-o"); const outputDirIndex = args.indexOf("-o");
@ -286,7 +281,6 @@ describe("PDFMathTranslate converter - Output structure", () => {
tarSourceDir = args[cIndex + 1]; tarSourceDir = args[cIndex + 1];
// Read the files in the archive source directory // Read the files in the archive source directory
if (existsSync(tarSourceDir)) { if (existsSync(tarSourceDir)) {
const { readdirSync } = require("node:fs");
archiveContents = readdirSync(tarSourceDir); archiveContents = readdirSync(tarSourceDir);
} }
} }
@ -309,7 +303,6 @@ describe("PDFMathTranslate converter - Output structure", () => {
cmd: string, cmd: string,
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "pdf2zh") { if (cmd === "pdf2zh") {
const outputDirIndex = args.indexOf("-o"); const outputDirIndex = args.indexOf("-o");
@ -362,9 +355,8 @@ describe("PDFMathTranslate converter - Error handling", () => {
test("should reject with error when pdf2zh fails", async () => { test("should reject with error when pdf2zh fails", async () => {
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
cmd: string, cmd: string,
args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "pdf2zh") { if (cmd === "pdf2zh") {
const error = new Error("Translation failed") as ExecFileException; const error = new Error("Translation failed") as ExecFileException;
@ -375,16 +367,15 @@ describe("PDFMathTranslate converter - Error handling", () => {
const targetPath = join(testDir, "output.tar"); const targetPath = join(testDir, "output.tar");
await expect( await expect(
convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile) convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile),
).rejects.toMatch(/pdf2zh error/); ).rejects.toThrow(/pdf2zh error|PDFMathTranslate error/);
}); });
test("should reject when no output PDF is generated", async () => { test("should reject when no output PDF is generated", async () => {
const mockExecFile: ExecFileFn = ( const mockExecFile: ExecFileFn = (
cmd: string, cmd: string,
args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
options?: any,
) => { ) => {
if (cmd === "pdf2zh") { if (cmd === "pdf2zh") {
// Don't create any output files // Don't create any output files
@ -397,7 +388,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
const targetPath = join(testDir, "output.tar"); const targetPath = join(testDir, "output.tar");
await expect( await expect(
convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile) convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile),
).rejects.toMatch(/No.*PDF.*found/); ).rejects.toThrow(/No.*PDF.*found|No translated PDF/);
}); });
}); });

View file

@ -13,7 +13,7 @@ import {
getChunk, getChunk,
createChunkDownloadHeaders, createChunkDownloadHeaders,
} from "../../src/transfer/downloadManager"; } from "../../src/transfer/downloadManager";
import { CHUNK_SIZE_BYTES, CHUNK_THRESHOLD_BYTES } from "../../src/transfer/constants"; import { CHUNK_SIZE_BYTES } from "../../src/transfer/constants";
const testDir = "./test-output-chunk-download"; const testDir = "./test-output-chunk-download";
@ -32,7 +32,6 @@ describe("Chunk 下載資訊測試", () => {
test("getChunkDownloadInfo 應返回正確的下載資訊", () => { test("getChunkDownloadInfo 應返回正確的下載資訊", () => {
const testFile = join(testDir, "large-file.bin"); const testFile = join(testDir, "large-file.bin");
const fileSize = 25 * 1024 * 1024; // 25MB
// 建立一個假檔案(只寫入少量資料模擬) // 建立一個假檔案(只寫入少量資料模擬)
const content = Buffer.alloc(1024, "X"); // 1KB const content = Buffer.alloc(1024, "X"); // 1KB

View file

@ -5,13 +5,9 @@
*/ */
import { test, expect, describe, beforeEach, afterEach } from "bun:test"; import { test, expect, describe, beforeEach, afterEach } from "bun:test";
import { mkdirSync, existsSync, writeFileSync, rmSync, readFileSync, statSync } from "node:fs"; import { mkdirSync, existsSync, rmSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { import { handleChunkUpload, calculateChunkCount } from "../../src/transfer/uploadManager";
handleChunkUpload,
uploadSessionManager,
calculateChunkCount,
} from "../../src/transfer/uploadManager";
import { CHUNK_SIZE_BYTES } from "../../src/transfer/constants"; import { CHUNK_SIZE_BYTES } from "../../src/transfer/constants";
const testDir = "./test-output-chunk-upload"; const testDir = "./test-output-chunk-upload";
@ -50,8 +46,16 @@ describe("Chunk 上傳整合測試", () => {
// 上傳 chunk 1 // 上傳 chunk 1
const result1 = await handleChunkUpload( const result1 = await handleChunkUpload(
uploadId, 0, totalChunks, chunk1, fileName, totalSize, uploadId,
userId, jobId, baseTempDir, targetDir 0,
totalChunks,
chunk1,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
); );
expect(result1.success).toBe(true); expect(result1.success).toBe(true);
expect(result1.completed).toBe(false); expect(result1.completed).toBe(false);
@ -59,16 +63,32 @@ describe("Chunk 上傳整合測試", () => {
// 上傳 chunk 2 // 上傳 chunk 2
const result2 = await handleChunkUpload( const result2 = await handleChunkUpload(
uploadId, 1, totalChunks, chunk2, fileName, totalSize, uploadId,
userId, jobId, baseTempDir, targetDir 1,
totalChunks,
chunk2,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
); );
expect(result2.success).toBe(true); expect(result2.success).toBe(true);
expect(result2.completed).toBe(false); expect(result2.completed).toBe(false);
// 上傳 chunk 3最後一個 // 上傳 chunk 3最後一個
const result3 = await handleChunkUpload( const result3 = await handleChunkUpload(
uploadId, 2, totalChunks, chunk3, fileName, totalSize, uploadId,
userId, jobId, baseTempDir, targetDir 2,
totalChunks,
chunk3,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
); );
expect(result3.success).toBe(true); expect(result3.success).toBe(true);
expect(result3.completed).toBe(true); expect(result3.completed).toBe(true);
@ -106,10 +126,54 @@ describe("Chunk 上傳整合測試", () => {
mkdirSync(targetDir, { recursive: true }); mkdirSync(targetDir, { recursive: true });
// 亂序上傳3, 1, 0, 2 // 亂序上傳3, 1, 0, 2
await handleChunkUpload(uploadId, 3, totalChunks, chunk3, fileName, totalSize, userId, jobId, baseTempDir, targetDir); await handleChunkUpload(
await handleChunkUpload(uploadId, 1, totalChunks, chunk1, fileName, totalSize, userId, jobId, baseTempDir, targetDir); uploadId,
await handleChunkUpload(uploadId, 0, totalChunks, chunk0, fileName, totalSize, userId, jobId, baseTempDir, targetDir); 3,
const finalResult = await handleChunkUpload(uploadId, 2, totalChunks, chunk2, fileName, totalSize, userId, jobId, baseTempDir, targetDir); totalChunks,
chunk3,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
);
await handleChunkUpload(
uploadId,
1,
totalChunks,
chunk1,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
);
await handleChunkUpload(
uploadId,
0,
totalChunks,
chunk0,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
);
const finalResult = await handleChunkUpload(
uploadId,
2,
totalChunks,
chunk2,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
);
expect(finalResult.success).toBe(true); expect(finalResult.success).toBe(true);
expect(finalResult.completed).toBe(true); expect(finalResult.completed).toBe(true);
@ -141,13 +205,46 @@ describe("Chunk 上傳整合測試", () => {
mkdirSync(targetDir, { recursive: true }); mkdirSync(targetDir, { recursive: true });
// 上傳 chunk 0 // 上傳 chunk 0
await handleChunkUpload(uploadId, 0, totalChunks, chunk0, fileName, totalSize, userId, jobId, baseTempDir, targetDir); await handleChunkUpload(
uploadId,
0,
totalChunks,
chunk0,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
);
// 重複上傳 chunk 0 // 重複上傳 chunk 0
await handleChunkUpload(uploadId, 0, totalChunks, chunk0, fileName, totalSize, userId, jobId, baseTempDir, targetDir); await handleChunkUpload(
uploadId,
0,
totalChunks,
chunk0,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
);
// 上傳 chunk 1 // 上傳 chunk 1
const result = await handleChunkUpload(uploadId, 1, totalChunks, chunk1, fileName, totalSize, userId, jobId, baseTempDir, targetDir); const result = await handleChunkUpload(
uploadId,
1,
totalChunks,
chunk1,
fileName,
totalSize,
userId,
jobId,
baseTempDir,
targetDir,
);
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.completed).toBe(true); expect(result.completed).toBe(true);

View file

@ -16,7 +16,6 @@ import {
shouldUseChunkedUpload, shouldUseChunkedUpload,
calculateChunkCount, calculateChunkCount,
handleDirectUpload, handleDirectUpload,
handleChunkUpload,
uploadSessionManager, uploadSessionManager,
} from "../../src/transfer/uploadManager"; } from "../../src/transfer/uploadManager";
import { import {
@ -148,7 +147,7 @@ describe("上傳管理測試", () => {
const testContent = "Hello, World!"; const testContent = "Hello, World!";
const file = new File([testContent], "test.txt", { type: "text/plain" }); const file = new File([testContent], "test.txt", { type: "text/plain" });
const result = await handleDirectUpload(file as any, targetDir, "test.txt"); const result = await handleDirectUpload(file, targetDir, "test.txt");
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(existsSync(join(targetDir, "test.txt"))).toBe(true); expect(existsSync(join(targetDir, "test.txt"))).toBe(true);
@ -309,7 +308,7 @@ describe("上傳會話管理測試", () => {
"large-file.pdf", "large-file.pdf",
50 * 1024 * 1024, // 50MB 50 * 1024 * 1024, // 50MB
10, // 10 chunks 10, // 10 chunks
testDir testDir,
); );
expect(session.upload_id).toBe(uploadId); expect(session.upload_id).toBe(uploadId);