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,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),
};
}

View file

@ -1,6 +1,6 @@
/**
* Contents.CN
*
*
*
*/

View file

@ -1,6 +1,6 @@
/**
* Contents.CN
*
*
*
* - 10MB
* - >10MBchunk
@ -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)}"`,
};
}

View file

@ -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";

View file

@ -2,24 +2,6 @@
* 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
*/

View file

@ -1,6 +1,6 @@
/**
* Contents.CN
*
*
*
* - 10MB
* - >10MBchunk
@ -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);
}