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,13 +1,13 @@
|
|||
/**
|
||||
* Contents.CN 封裝管理器
|
||||
*
|
||||
*
|
||||
* 統一處理多檔輸出的封裝:
|
||||
* - 唯一允許格式:.tar
|
||||
* - 禁止:.tar.gz, .tgz, .zip 等
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, basename } from "node:path";
|
||||
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import * as tar from "tar";
|
||||
import { ALLOWED_ARCHIVE_FORMAT, FORBIDDEN_ARCHIVE_FORMATS } from "./constants";
|
||||
|
||||
|
|
@ -16,14 +16,14 @@ import { ALLOWED_ARCHIVE_FORMAT, FORBIDDEN_ARCHIVE_FORMATS } from "./constants";
|
|||
*/
|
||||
export function validateArchiveFormat(fileName: string): boolean {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
|
||||
|
||||
// 檢查是否使用禁止的格式
|
||||
for (const forbidden of FORBIDDEN_ARCHIVE_FORMATS) {
|
||||
if (lowerName.endsWith(forbidden)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 檢查是否是允許的格式
|
||||
return lowerName.endsWith(ALLOWED_ARCHIVE_FORMAT);
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ export function validateArchiveFormat(fileName: string): boolean {
|
|||
*/
|
||||
export function getArchiveFileName(baseName: string): string {
|
||||
const lowerName = baseName.toLowerCase();
|
||||
|
||||
|
||||
// 移除任何禁止的副檔名
|
||||
let cleanName = baseName;
|
||||
for (const forbidden of FORBIDDEN_ARCHIVE_FORMATS) {
|
||||
|
|
@ -42,19 +42,19 @@ export function getArchiveFileName(baseName: string): string {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果已經是 .tar 結尾,直接返回
|
||||
if (cleanName.toLowerCase().endsWith(ALLOWED_ARCHIVE_FORMAT)) {
|
||||
return cleanName;
|
||||
}
|
||||
|
||||
|
||||
// 否則加上 .tar
|
||||
return `${cleanName}${ALLOWED_ARCHIVE_FORMAT}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立 .tar 封裝(不壓縮)
|
||||
*
|
||||
*
|
||||
* @param sourceDir - 來源目錄
|
||||
* @param outputPath - 輸出的 .tar 檔案路徑
|
||||
* @param options - 額外選項
|
||||
|
|
@ -65,11 +65,11 @@ export async function createTarArchive(
|
|||
options: {
|
||||
filter?: (path: string) => boolean;
|
||||
prefix?: string;
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<string> {
|
||||
// 強制確保輸出是 .tar 格式
|
||||
const finalOutputPath = getArchiveFileName(outputPath);
|
||||
|
||||
|
||||
// 確保輸出目錄存在
|
||||
const outputDir = join(finalOutputPath, "..");
|
||||
if (!existsSync(outputDir)) {
|
||||
|
|
@ -78,7 +78,7 @@ export async function createTarArchive(
|
|||
|
||||
// 取得要封裝的檔案列表
|
||||
const files = readdirSync(sourceDir);
|
||||
|
||||
|
||||
// 預設過濾器:排除其他 tar 檔案
|
||||
const defaultFilter = (path: string) => !path.match(/\.tar$/i);
|
||||
const filter = options.filter || defaultFilter;
|
||||
|
|
@ -91,7 +91,7 @@ export async function createTarArchive(
|
|||
// 不使用 gzip 壓縮
|
||||
gzip: false,
|
||||
},
|
||||
files.filter(f => filter(f))
|
||||
files.filter((f) => filter(f)),
|
||||
);
|
||||
|
||||
console.log(`Created tar archive: ${finalOutputPath}`);
|
||||
|
|
@ -100,7 +100,7 @@ export async function createTarArchive(
|
|||
|
||||
/**
|
||||
* 建立用於多檔輸出的 .tar 封裝
|
||||
*
|
||||
*
|
||||
* @param outputDir - 輸出目錄(包含多個轉換結果)
|
||||
* @param jobId - 任務 ID
|
||||
*/
|
||||
|
|
@ -114,41 +114,3 @@ export async function createJobArchive(outputDir: string, jobId: string): Promis
|
|||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Contents.CN 全域檔案傳輸常數
|
||||
*
|
||||
*
|
||||
* 這些常數定義了整個系統的檔案傳輸規則
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Contents.CN 後端下載管理器
|
||||
*
|
||||
*
|
||||
* 統一處理所有檔案下載,包含:
|
||||
* - 小檔(≤10MB):直接回傳
|
||||
* - 大檔(>10MB):chunk 分段下載
|
||||
|
|
@ -46,9 +46,9 @@ export function getChunkDownloadInfo(filePath: string): ChunkDownloadInfo | null
|
|||
* 取得特定 chunk 的資料
|
||||
*/
|
||||
export async function getChunk(
|
||||
filePath: string,
|
||||
chunkIndex: number,
|
||||
chunkSize: number = CHUNK_SIZE_BYTES
|
||||
filePath: string,
|
||||
chunkIndex: number,
|
||||
chunkSize: number = CHUNK_SIZE_BYTES,
|
||||
): Promise<Buffer | null> {
|
||||
if (!existsSync(filePath)) {
|
||||
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
|
||||
*/
|
||||
export function createChunkDownloadHeaders(
|
||||
info: ChunkDownloadInfo,
|
||||
chunkIndex: number,
|
||||
chunkData: Buffer
|
||||
info: ChunkDownloadInfo,
|
||||
chunkIndex: number,
|
||||
chunkData: Buffer,
|
||||
): Record<string, string> {
|
||||
const start = chunkIndex * info.chunk_size;
|
||||
const end = start + chunkData.length - 1;
|
||||
|
|
@ -110,14 +100,3 @@ export function createChunkDownloadHeaders(
|
|||
"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)}"`,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Contents.CN 全域檔案傳輸模組
|
||||
*
|
||||
*
|
||||
* 統一匯出所有傳輸相關功能
|
||||
*/
|
||||
|
||||
|
|
@ -16,7 +16,6 @@ export {
|
|||
|
||||
// 類型
|
||||
export type {
|
||||
ChunkUploadRequest,
|
||||
ChunkUploadResponse,
|
||||
DirectUploadResponse,
|
||||
ChunkDownloadInfo,
|
||||
|
|
@ -40,9 +39,7 @@ export {
|
|||
shouldUseChunkedDownload,
|
||||
getChunkDownloadInfo,
|
||||
getChunk,
|
||||
getFileForDirectDownload,
|
||||
createChunkDownloadHeaders,
|
||||
createDirectDownloadHeaders,
|
||||
} from "./downloadManager";
|
||||
|
||||
// 封裝管理
|
||||
|
|
@ -51,6 +48,4 @@ export {
|
|||
getArchiveFileName,
|
||||
createTarArchive,
|
||||
createJobArchive,
|
||||
createConverterArchive,
|
||||
getArchiveInfo,
|
||||
} from "./archiveManager";
|
||||
|
|
|
|||
|
|
@ -2,24 +2,6 @@
|
|||
* 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 上傳回應結構
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Contents.CN 後端上傳管理器
|
||||
*
|
||||
*
|
||||
* 統一處理所有檔案上傳,包含:
|
||||
* - 小檔(≤10MB):直接接收
|
||||
* - 大檔(>10MB):chunk 接收與合併
|
||||
|
|
@ -8,7 +8,12 @@
|
|||
|
||||
import { existsSync, mkdirSync, rmSync, readdirSync, createWriteStream, statSync } from "node:fs";
|
||||
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 { getTransferMode } from "./types";
|
||||
|
||||
|
|
@ -22,9 +27,12 @@ class UploadSessionManager {
|
|||
|
||||
constructor() {
|
||||
// 定期清理過期的 sessions
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.cleanupExpiredSessions();
|
||||
}, 5 * 60 * 1000); // 每 5 分鐘清理一次
|
||||
this.cleanupInterval = setInterval(
|
||||
() => {
|
||||
this.cleanupExpiredSessions();
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
); // 每 5 分鐘清理一次
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -37,10 +45,10 @@ class UploadSessionManager {
|
|||
fileName: string,
|
||||
totalSize: number,
|
||||
totalChunks: number,
|
||||
baseTempDir: string
|
||||
baseTempDir: string,
|
||||
): UploadSession {
|
||||
const tempDir = join(baseTempDir, CHUNK_TEMP_DIR, uploadId);
|
||||
|
||||
|
||||
if (!existsSync(tempDir)) {
|
||||
mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
|
@ -74,7 +82,7 @@ class UploadSessionManager {
|
|||
markChunkReceived(uploadId: string, chunkIndex: number): boolean {
|
||||
const session = this.sessions.get(uploadId);
|
||||
if (!session) return false;
|
||||
|
||||
|
||||
session.received_chunks.add(chunkIndex);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -85,7 +93,7 @@ class UploadSessionManager {
|
|||
isComplete(uploadId: string): boolean {
|
||||
const session = this.sessions.get(uploadId);
|
||||
if (!session) return false;
|
||||
|
||||
|
||||
return session.received_chunks.size === session.total_chunks;
|
||||
}
|
||||
|
||||
|
|
@ -140,11 +148,11 @@ export function shouldUseChunkedUpload(fileSize: number): boolean {
|
|||
export async function handleDirectUpload(
|
||||
file: File,
|
||||
targetDir: string,
|
||||
fileName: string
|
||||
fileName: string,
|
||||
): Promise<DirectUploadResponse> {
|
||||
try {
|
||||
const targetPath = join(targetDir, fileName);
|
||||
|
||||
|
||||
if (!existsSync(targetDir)) {
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
|
@ -178,12 +186,12 @@ export async function handleChunkUpload(
|
|||
userId: string,
|
||||
jobId: string,
|
||||
baseTempDir: string,
|
||||
targetDir: string
|
||||
targetDir: string,
|
||||
): Promise<ChunkUploadResponse> {
|
||||
try {
|
||||
// 取得或建立會話
|
||||
let session = uploadSessionManager.getSession(uploadId);
|
||||
|
||||
|
||||
if (!session) {
|
||||
session = uploadSessionManager.createSession(
|
||||
uploadId,
|
||||
|
|
@ -192,7 +200,7 @@ export async function handleChunkUpload(
|
|||
fileName,
|
||||
totalSize,
|
||||
totalChunks,
|
||||
baseTempDir
|
||||
baseTempDir,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +232,7 @@ export async function handleChunkUpload(
|
|||
if (uploadSessionManager.isComplete(uploadId)) {
|
||||
// 合併所有 chunks
|
||||
const finalPath = await mergeChunks(session, targetDir);
|
||||
|
||||
|
||||
// 清理會話
|
||||
uploadSessionManager.removeSession(uploadId);
|
||||
|
||||
|
|
@ -257,14 +265,14 @@ export async function handleChunkUpload(
|
|||
*/
|
||||
async function mergeChunks(session: UploadSession, targetDir: string): Promise<string> {
|
||||
const targetPath = join(targetDir, session.file_name);
|
||||
|
||||
|
||||
if (!existsSync(targetDir)) {
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 讀取並排序所有 chunks
|
||||
const chunkFiles = readdirSync(session.temp_dir)
|
||||
.filter(f => f.startsWith("chunk_"))
|
||||
.filter((f) => f.startsWith("chunk_"))
|
||||
.sort();
|
||||
|
||||
// 建立輸出串流
|
||||
|
|
@ -298,6 +306,9 @@ async function mergeChunks(session: UploadSession, targetDir: string): Promise<s
|
|||
/**
|
||||
* 計算需要的 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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue