gongxiangp2/lib/disk-space.ts

174 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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'
}