chore: initial commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import {lstat, opendir} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {performance} from 'node:perf_hooks';
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import {get_project_root_path} from '../../../utils/path_utils.ts';
|
||||
import type {InfoContext} from '../types.ts';
|
||||
import {metadata, ResourceInfo} from '../resource_metrics.ts';
|
||||
|
||||
export interface StorageInfo {
|
||||
available: boolean;
|
||||
total_bytes: number | null;
|
||||
file_count: number;
|
||||
partial: boolean;
|
||||
}
|
||||
|
||||
/** lstat and streamed directory enumeration never intentionally follow symlinks. Live mutation can race a scan. */
|
||||
export async function scan_storage(directory: string, cancelled = () => false,
|
||||
limits = {entries: 20000, depth: 32, milliseconds: 2000}): Promise<StorageInfo> {
|
||||
const info: StorageInfo = {available: false, total_bytes: null, file_count: 0, partial: false};
|
||||
const start = performance.now();
|
||||
let entries = 0;
|
||||
const pending: [string, number][] = [[directory, 0]];
|
||||
while (pending.length) {
|
||||
if (cancelled() || entries >= limits.entries || performance.now() - start >= limits.milliseconds) {
|
||||
info.partial = true; break;
|
||||
}
|
||||
const [current, depth] = pending.pop()!;
|
||||
entries++;
|
||||
try {
|
||||
const stat = await lstat(current);
|
||||
if (current === directory) {
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) { info.partial = true; break; }
|
||||
info.available = true;
|
||||
info.total_bytes = 0;
|
||||
}
|
||||
if (stat.isSymbolicLink()) continue;
|
||||
if (stat.isFile()) {
|
||||
info.total_bytes! += stat.size;
|
||||
info.file_count++;
|
||||
} else if (stat.isDirectory()) {
|
||||
if (depth >= limits.depth) { info.partial = true; continue; }
|
||||
const dir = await opendir(current);
|
||||
for await (const entry of dir) {
|
||||
if (cancelled() || entries + pending.length >= limits.entries
|
||||
|| performance.now() - start >= limits.milliseconds) { info.partial = true; break; }
|
||||
if (!entry.isSymbolicLink()) pending.push([path.join(current, entry.name), depth + 1]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (current === directory && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
info.available = true;
|
||||
info.total_bytes = 0;
|
||||
} else info.partial = true;
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
export class init extends ResourceInfo<StorageInfo> {
|
||||
readonly name = 'used_storage';
|
||||
readonly i18n = metadata('机器人数据存储', '统计 data/bot/<bot_id> 下文件的逻辑大小,跳过符号链接。'
|
||||
+ '每 30 秒尽力扫描一次,不重叠执行;上限为 20000 个条目、32 层深度和 2 秒。'
|
||||
+ '扫描期间的并发文件变动可能影响结果;该指标不代表实际分配的磁盘空间。', {
|
||||
available: ['存储统计是否可用', '目录可访问或不存在时为 true,不存在按 0 统计;缺少有效机器人编号或根目录不可访问时为 false'],
|
||||
total_bytes: ['文件总大小', '已观察到的普通文件逻辑大小之和,单位为字节;不可用时为 null'],
|
||||
file_count: ['文件数量', '已观察到的普通文件数量'],
|
||||
partial: ['结果是否不完整', '因扫描上限、取消或错误而未完成扫描;保留已读取文件的统计小计'],
|
||||
});
|
||||
constructor(_bot: Bot, options?: InfoContext) {
|
||||
super({available: false, total_bytes: null, file_count: 0, partial: true});
|
||||
if (!Number.isSafeInteger(options?.bot_id) || options!.bot_id! <= 0) return;
|
||||
const directory = path.join(get_project_root_path(), 'data', 'bot', String(options!.bot_id));
|
||||
this.start(30000, async () => { this.publish(await scan_storage(directory, () => this.disposed)); }, true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user