import { lstat, open } from 'fs/promises' export interface BoundedFileReadOptions { maxBytes: number expectedBytes?: number label?: string } interface ValidatedRegularFile { size: number } function assertSafeByteLimit(value: number, name: string): void { if (!Number.isSafeInteger(value) || value <= 0) { throw new Error(`${name}无效`) } } function validateRegularFileSize( size: number, options: BoundedFileReadOptions, ): ValidatedRegularFile { const label = options.label || '文件' assertSafeByteLimit(options.maxBytes, `${label}大小上限`) if (options.expectedBytes !== undefined) { assertSafeByteLimit(options.expectedBytes, `${label}申报大小`) } if (!Number.isSafeInteger(size) || size <= 0) { throw new Error(`${label}为空或大小无效`) } if (size > options.maxBytes) { throw new Error(`${label}超过 ${Math.ceil(options.maxBytes / 1024 / 1024)}MB 限制`) } if (options.expectedBytes !== undefined && size !== options.expectedBytes) { throw new Error(`${label}实际大小与上传记录不一致,请重新上传`) } return { size } } /** * 在真正分配 Buffer 前检查路径与实际文件大小。调用方不能只信浏览器传入的 * fileSize;上传会话最高可到数 GiB,直接 readFile 会让 Node 进程被 OOM 杀死。 */ export async function inspectBoundedRegularFile( filePath: string, options: BoundedFileReadOptions, ): Promise { const label = options.label || '文件' const pathStats = await lstat(filePath) if (pathStats.isSymbolicLink() || !pathStats.isFile()) { throw new Error(`${label}不是可读取的普通文件`) } return validateRegularFileSize(pathStats.size, options) } /** * 通过已打开的 FileHandle 读取,打开后再次 fstat,避免检查路径后文件被替换 * 或扩大的窗口。读取完成后还会核对字节数,任何不确定状态都失败关闭。 */ export async function readBoundedRegularFile( filePath: string, options: BoundedFileReadOptions, ): Promise { await inspectBoundedRegularFile(filePath, options) const label = options.label || '文件' const handle = await open(filePath, 'r') try { const openedStats = await handle.stat() if (!openedStats.isFile()) { throw new Error(`${label}不是可读取的普通文件`) } const validated = validateRegularFileSize(openedStats.size, options) const buffer = await handle.readFile() if (buffer.length !== validated.size) { throw new Error(`${label}读取期间发生变化,请重新上传`) } return buffer } finally { await handle.close() } } /** * 受限读取 fetch 响应。先核对 Content-Length,再按声明大小预分配单个 Buffer, * 不使用 arrayBuffer/Buffer.concat,防止未知长度响应造成无上限内存增长。 */ export async function readBoundedResponseBuffer( response: Response, options: BoundedFileReadOptions, ): Promise { const label = options.label || '远程文件' assertSafeByteLimit(options.maxBytes, `${label}大小上限`) if (options.expectedBytes === undefined) { throw new Error(`${label}缺少可信大小,拒绝载入内存`) } assertSafeByteLimit(options.expectedBytes, `${label}申报大小`) if (options.expectedBytes > options.maxBytes) { throw new Error(`${label}超过 ${Math.ceil(options.maxBytes / 1024 / 1024)}MB 限制`) } const rawContentLength = response.headers.get('content-length') if (rawContentLength !== null) { const contentLength = Number(rawContentLength) if (!Number.isSafeInteger(contentLength) || contentLength !== options.expectedBytes) { throw new Error(`${label}响应大小与上传记录不一致`) } } if (!response.body) { throw new Error(`${label}响应内容为空`) } const output = Buffer.allocUnsafe(options.expectedBytes) const reader = response.body.getReader() let offset = 0 try { while (true) { const { done, value } = await reader.read() if (done) { break } if (!value || value.byteLength === 0) { continue } if (offset + value.byteLength > options.expectedBytes) { throw new Error(`${label}响应超过申报大小`) } output.set(value, offset) offset += value.byteLength } } catch (error) { await reader.cancel(error).catch(() => undefined) throw error } finally { reader.releaseLock() } if (offset !== options.expectedBytes) { throw new Error(`${label}响应不完整`) } return output }