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:
parent
db5f47586e
commit
79fc6f7067
21 changed files with 439 additions and 439 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,10 +28,7 @@ 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",
|
|
||||||
["-cf", outputTar, "-C", sourceDir, "."],
|
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`tar error: ${error}`);
|
reject(`tar error: ${error}`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -43,8 +40,7 @@ function createTarArchive(
|
||||||
console.error(`tar stderr: ${stderr}`);
|
console.error(`tar stderr: ${stderr}`);
|
||||||
}
|
}
|
||||||
resolve();
|
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") {
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,15 @@ const SUPPORTED_LANGUAGES = [
|
||||||
"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,10 +104,7 @@ 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",
|
|
||||||
["-cf", outputTar, "-C", sourceDir, "."],
|
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`tar error: ${error}`);
|
reject(`tar error: ${error}`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -118,8 +116,7 @@ function createTarArchive(
|
||||||
console.error(`tar stderr: ${stderr}`);
|
console.error(`tar stderr: ${stderr}`);
|
||||||
}
|
}
|
||||||
resolve();
|
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,10 +234,9 @@ 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();
|
||||||
|
|
@ -290,7 +289,7 @@ export async function convert(
|
||||||
// 如果找不到預期的輸出檔案,嘗試找任何 PDF
|
// 如果找不到預期的輸出檔案,嘗試找任何 PDF
|
||||||
if (!foundOutput) {
|
if (!foundOutput) {
|
||||||
const allFiles = readdirSync(tempDir);
|
const allFiles = readdirSync(tempDir);
|
||||||
const pdfFiles = allFiles.filter(f => f.endsWith(".pdf") && f !== basename(filePath));
|
const pdfFiles = allFiles.filter((f) => f.endsWith(".pdf") && f !== basename(filePath));
|
||||||
const firstPdfName = pdfFiles[0];
|
const firstPdfName = pdfFiles[0];
|
||||||
|
|
||||||
if (firstPdfName) {
|
if (firstPdfName) {
|
||||||
|
|
@ -318,9 +317,8 @@ export async function convert(
|
||||||
// 9. 清理臨時目錄
|
// 9. 清理臨時目錄
|
||||||
removeDir(tempDir);
|
removeDir(tempDir);
|
||||||
|
|
||||||
resolve("Done");
|
return "Done";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(`PDFMathTranslate error: ${error}`);
|
throw new Error(`PDFMathTranslate error: ${error}`);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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)}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,6 @@
|
||||||
* Contents.CN 檔案傳輸類型定義
|
* Contents.CN 檔案傳輸類型定義
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Chunk 上傳請求結構
|
|
||||||
*/
|
|
||||||
export interface ChunkUploadRequest {
|
|
||||||
/** 上傳會話 ID(UUID) */
|
|
||||||
upload_id: string;
|
|
||||||
/** 當前 chunk 索引(從 0 開始) */
|
|
||||||
chunk_index: number;
|
|
||||||
/** 總 chunk 數量 */
|
|
||||||
total_chunks: number;
|
|
||||||
/** chunk 數據 */
|
|
||||||
data: Blob | ArrayBuffer;
|
|
||||||
/** 原始檔案名稱 */
|
|
||||||
file_name: string;
|
|
||||||
/** 檔案總大小 */
|
|
||||||
total_size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chunk 上傳回應結構
|
* Chunk 上傳回應結構
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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();
|
this.cleanupExpiredSessions();
|
||||||
}, 5 * 60 * 1000); // 每 5 分鐘清理一次
|
},
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue