174 lines
5.3 KiB
TypeScript
174 lines
5.3 KiB
TypeScript
/**
|
||
* 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<DiskSpaceInfo | null> {
|
||
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<boolean> {
|
||
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<void> {
|
||
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'
|
||
}
|