Files
openclaw/src/infra/disk-space.ts
2026-05-01 23:23:55 +01:00

86 lines
2.5 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
const LOW_DISK_SPACE_WARNING_THRESHOLD_BYTES = 1024 * 1024 * 1024;
type DiskSpaceSnapshot = {
targetPath: string;
checkedPath: string;
availableBytes: number;
totalBytes: number | null;
};
function finiteNonNegativeNumber(value: unknown): number | null {
const numberValue = Number(value);
return Number.isFinite(numberValue) && numberValue >= 0 ? numberValue : null;
}
function findExistingDiskSpacePath(targetPath: string): string | null {
let current = path.resolve(targetPath);
while (true) {
try {
const stats = fs.statSync(current);
return stats.isDirectory() ? current : path.dirname(current);
} catch {
const parent = path.dirname(current);
if (parent === current) {
return null;
}
current = parent;
}
}
}
export function tryReadDiskSpace(targetPath: string): DiskSpaceSnapshot | null {
if (typeof fs.statfsSync !== "function") {
return null;
}
const checkedPath = findExistingDiskSpacePath(targetPath);
if (!checkedPath) {
return null;
}
try {
const stats = fs.statfsSync(checkedPath);
const blockSize = finiteNonNegativeNumber(stats.bsize);
const availableBlocks = finiteNonNegativeNumber(stats.bavail);
if (blockSize === null || availableBlocks === null) {
return null;
}
const totalBlocks = finiteNonNegativeNumber(stats.blocks);
return {
targetPath,
checkedPath,
availableBytes: blockSize * availableBlocks,
totalBytes: totalBlocks === null ? null : blockSize * totalBlocks,
};
} catch {
return null;
}
}
export function formatDiskSpaceBytes(bytes: number): string {
const mib = bytes / (1024 * 1024);
if (mib < 1024) {
return `${Math.max(0, Math.round(mib))} MiB`;
}
const gib = mib / 1024;
return `${gib.toFixed(gib < 10 ? 1 : 0)} GiB`;
}
export function createLowDiskSpaceWarning(params: {
targetPath: string;
purpose: string;
thresholdBytes?: number;
}): string | null {
const thresholdBytes = params.thresholdBytes ?? LOW_DISK_SPACE_WARNING_THRESHOLD_BYTES;
const snapshot = tryReadDiskSpace(params.targetPath);
if (!snapshot || snapshot.availableBytes >= thresholdBytes) {
return null;
}
const location =
path.resolve(snapshot.targetPath) === path.resolve(snapshot.checkedPath)
? snapshot.checkedPath
: `${snapshot.targetPath} (volume checked at ${snapshot.checkedPath})`;
return `Low disk space near ${location}: ${formatDiskSpaceBytes(snapshot.availableBytes)} available; ${params.purpose} may fail.`;
}