/** * R38修复:磁盘空间检查工具 * * 用途:在大文件操作前检查磁盘剩余空间,防止因磁盘写满导致服务崩溃 * * 使用 Node.js 原生 statfs,不依赖 GNU/BusyBox 命令参数或文本输出格式。 */ import { statfs } from 'fs/promises' interface DiskSpaceInfo { /** 磁盘总容量(字节) */ total: number /** 已使用空间(字节) */ used: number /** 可用空间(字节) */ available: number /** 使用百分比 (0-100) */ usePercent: number /** 挂载点/盘符 */ mountPoint: string } function clampToSafeNumber(value: bigint): number { if (value <= BigInt(0)) return 0 const maxSafeInteger = BigInt(Number.MAX_SAFE_INTEGER) return value > maxSafeInteger ? Number.MAX_SAFE_INTEGER : Number(value) } /** * 检查磁盘空间 * @param path 要检查的路径(默认为临时目录) * @returns 磁盘空间信息,失败返回 null */ export async function checkDiskSpace(path: string = require('os').tmpdir()): Promise { try { const space = await statfs(path, { bigint: true }) const zero = BigInt(0) const blockSize = space.bsize > zero ? space.bsize : zero const totalBytes = space.blocks > zero ? space.blocks * blockSize : zero const freeBytes = space.bfree > zero ? space.bfree * blockSize : zero const availableBytes = space.bavail > zero ? space.bavail * blockSize : zero const usedBytes = totalBytes > freeBytes ? totalBytes - freeBytes : zero // 与 df 的 Use% 语义一致:非特权进程不能使用的保留块不进入分母。 // 绝对空间门禁同样使用 bavail,避免把 root 保留空间误算给 nextjs。 const usableBytes = usedBytes + availableBytes const usePercent = usableBytes > zero ? Number((usedBytes * BigInt(100) + usableBytes - BigInt(1)) / usableBytes) : 0 return { total: clampToSafeNumber(totalBytes), used: clampToSafeNumber(usedBytes), available: clampToSafeNumber(availableBytes), usePercent: Math.min(100, Math.max(0, usePercent)), // statfs 不返回挂载点;保留被检查路径用于日志与诊断。 mountPoint: path, } } catch (error) { console.error('[DiskSpace] Failed to check disk space:', error) return null } } /** * 检查是否有足够磁盘空间 * @param requiredBytes 需要的字节数 * @param path 检查的路径 * @param minFreePercent 最小剩余空间百分比(默认 10%) * @returns 是否有足够空间 */ export async function hasEnoughDiskSpace( requiredBytes: number, path?: string, minFreePercent: number = 10 ): Promise { const space = await checkDiskSpace(path) if (!space) { // 无法检查磁盘空间时,保守处理:假设有足够空间 console.warn('[DiskSpace] Unable to check disk space, assuming sufficient space') return true } // 检查绝对值 if (space.available < requiredBytes) { console.warn( `[DiskSpace] Insufficient disk space: need ${formatBytes(requiredBytes)}, available ${formatBytes(space.available)}` ) return false } // 检查百分比(确保不会把磁盘填满) if (space.usePercent > (100 - minFreePercent)) { console.warn( `[DiskSpace] Disk usage too high: ${space.usePercent}% (max allowed: ${100 - minFreePercent}%)` ) return false } return true } /** * R38修复:检查磁盘空间并在不足时抛出错误 * @param requiredBytes 需要的字节数 * @param operation 操作名称(用于错误消息) * @param path 检查的路径 * @throws Error 当磁盘空间不足时 */ export async function ensureEnoughDiskSpace( requiredBytes: number, operation: string = '操作', path?: string ): Promise { const space = await checkDiskSpace(path) if (!space) { console.warn(`[DiskSpace] Unable to check disk space before ${operation}, proceeding anyway`) return } // 计算最小保留空间(磁盘总容量的 10%,或至少 5GB) const minReservedBytes = Math.max(space.total * 0.1, 5 * 1024 * 1024 * 1024) const requiredWithReserve = requiredBytes + minReservedBytes if (space.available < requiredWithReserve) { throw new Error( `磁盘空间不足,无法${operation}。` + `需要 ${formatBytes(requiredBytes)},` + `但只剩余 ${formatBytes(space.available)} ` + `(使用率 ${space.usePercent}%)。` + `请清理磁盘空间后再试。` ) } console.log( `[DiskSpace] Disk space check passed for ${operation}: ` + `need ${formatBytes(requiredBytes)}, available ${formatBytes(space.available)} (${space.usePercent}% used)` ) } /** * 格式化字节数为可读格式 */ export function formatBytes(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB', 'TB'] let size = bytes let unitIndex = 0 while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024 unitIndex++ } return `${size.toFixed(2)} ${units[unitIndex]}` } /** * 获取磁盘使用率警告级别 * @returns 'ok' (正常), 'warning' (>70%), 'critical' (>90%) */ export async function getDiskUsageLevel(path?: string): Promise<'ok' | 'warning' | 'critical'> { const space = await checkDiskSpace(path) if (!space) { return 'ok' } if (space.usePercent >= 90) { return 'critical' } else if (space.usePercent >= 70) { return 'warning' } return 'ok' }