UNPKG

get-folder

Version:

High-Performance Folder Size Calculator.

456 lines (445 loc) 13.7 kB
/*! * get-folder v0.1.1 * High-Performance Folder Size Calculator. * Copyright (c) jl15988. * This source code is licensed under the MIT license. */ 'use strict'; var fs = require('fs'); var path$1 = require('path'); var bignumber_js = require('bignumber.js'); var path = require('node:path'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path); /** * 简化的信号量类,用于控制并发数量 * * 工作原理: * 1. 维护一个固定数量的"令牌"(tokens) * 2. 每个并发操作需要先获取令牌才能执行 * 3. 没有令牌时,操作会被挂起等待 * 4. 操作完成后释放令牌,唤醒等待的操作 * * 这样确保同时运行的操作数量不会超过设定的并发限制, * 避免文件句柄耗尽、内存占用过高等问题 */ class SimpleSemaphore { /** * 当前可用的令牌数量 */ available; /** * 等待队列:存储等待令牌的 resolve 函数 */ waiters = []; /** * 构造函数 * @param concurrency 最大并发数(令牌总数) */ constructor(concurrency) { this.available = concurrency; } /** * 获取令牌(申请执行权限) * * 执行逻辑: * 1. 如果有可用令牌,立即获取并返回 * 2. 如果没有令牌,创建 Promise 并加入等待队列 * 3. 当前操作会被挂起,直到有令牌释放时被唤醒 * * @returns Promise<void> 当获得令牌时 resolve */ async acquire() { // 情况1:有可用令牌,立即获取 if (this.available > 0) { // 消耗一个令牌 this.available--; // 立即返回,继续执行 return; } // 情况2:没有可用令牌,需要等待 return new Promise((resolve) => { // 将 resolve 函数包装后放入等待队列 // 当有令牌释放时,会调用这个函数唤醒等待者 this.waiters.push(() => { // 消耗令牌 this.available--; // 唤醒等待的 acquire() 调用 resolve(); }); }); } /** * 释放令牌(归还执行权限) * * 执行逻辑: * 1. 优先唤醒等待队列中的第一个等待者 * 2. 如果没有等待者,增加可用令牌数 * * 这样确保令牌总数始终保持不变 */ release() { // 情况1:有等待者,立即唤醒第一个 if (this.waiters.length > 0) { // 取出队列头部的 resolve 函数 const resolve = this.waiters.shift(); // 调用 resolve,唤醒对应的 acquire() 调用 resolve(); // 注意:这里不增加 available,因为令牌直接转给了等待者 } // 情况2:没有等待者,归还令牌到令牌池 else { // 增加可用令牌数 this.available++; } } } /** * 路径工具 */ class PathUtil { /** * 判断文件路径是否为隐藏文件(简化实现,仅判断是否以'.'开头) * @param filePath 文件路径 */ static isHiddenFile(filePath) { const name = path__namespace.basename(filePath); // 空文件名或当前目录/上级目录 if (!name || name === '.' || name === '..') { return false; } // 跨平台检查:以 . 开头的文件 return name.startsWith('.'); } } /** * 正则工具 */ class RegExpUtil { /** * 测试 val 是否符合 testRegs 规则 * @param testRegs 正则规则 * @param val 值 */ static tests(testRegs, val) { return testRegs.some(pattern => pattern.test(val)); } } class BaseScene { /** * 已处理的硬链接 */ processedInodes = new Set(); /** * 检查是否应该忽略指定路径 * @param ignores 忽略列表 * @param itemPath 项目路径 * @param includeHidden 是否包含隐藏文件 * @returns 是否应该忽略 */ shouldIgnorePath(ignores, itemPath, includeHidden = true) { // 检查隐藏文件 if (!includeHidden && PathUtil.isHiddenFile(itemPath)) { return true; } if (ignores && ignores.length > 0) { // 检查忽略模式 return RegExpUtil.tests(ignores, itemPath); } return false; } /** * 获取inode关键值,如果stats为字符串让直接返回 * @param stats fs统计信息 */ getInodeKey(stats) { if (typeof stats === "string") { return stats; } return `${stats.dev}-${stats.ino}`; } /** * 判断是否已有 inode * @param inode inode */ hasInode(inode) { return this.processedInodes.has(this.getInodeKey(inode)); } /** * 添加 inode * @param inode inode */ addInode(inode) { this.processedInodes.add(this.getInodeKey(inode)); } /** * 检查 inode,用于防止硬链接重复 * * 如果已有inode将返回true,否则记录inode并返回false * @param stats fs统计信息 */ checkInode(stats) { const inodeKey = this.getInodeKey(stats); if (this.hasInode(inodeKey)) { return true; } this.addInode(inodeKey); return false; } /** * 清除 inode */ clearInode() { this.processedInodes.clear(); } } /** * 文件夹大小计算器类 */ class FolderSize extends BaseScene { options; /** * 快捷获取文件夹大小 * @param folderPath 文件夹路径 * @param options 大小计算选项 * @returns 文件夹大小结果 Promise */ static getSize(folderPath, options = {}) { return FolderSize.of(options).size(folderPath); } /** * 与构造函数一致 * @param options 大小计算选项 */ static of(options = {}) { return new FolderSize(options); } /** * 构造函数 * @param options 大小计算选项 */ constructor(options = {}) { super(); this.options = { maxDepth: Number.MAX_SAFE_INTEGER, ignores: [], includeHidden: true, includeLink: true, concurrency: 2, ignoreErrors: false, inodeCheck: true, // 默认继续 onError: () => true, ...options }; } /** * 计算文件夹大小 * @param folderPath 文件夹路径 * @returns 文件夹大小结果 Promise */ async size(folderPath) { this.clearInode(); const normalizePath = path__namespace.normalize(folderPath); // 使用 Node.js 实现 return await this.calculateSizeNodeJs(normalizePath); } /** * 计算文件夹大小 * @param folderPath 文件夹路径 * @returns 计算结果 */ async calculateSizeNodeJs(folderPath) { let totalSize = new bignumber_js.BigNumber(0); let fileCount = 0; let directoryCount = 0; let linkCount = 0; /** * 递归处理文件夹项目 * @param itemPath 项目路径 * @param depth 当前深度 */ const processItem = async (itemPath, depth = 0) => { // 检查是否应该忽略此路径 if (this.shouldIgnorePath(this.options.ignores, itemPath, this.options.includeHidden)) { return; } let stats; try { // 获取文件统计信息 stats = await fs.promises.lstat(itemPath, { bigint: true }); } catch (error) { return this.handleError(error, `无法获取文件信息: ${error.message}`, itemPath); } if (this.options.inodeCheck) { if (this.checkInode(stats)) return; } const isSymbolicLink = stats.isSymbolicLink(); const isDirectory = stats.isDirectory(); const isFile = stats.isFile(); const fileSize = stats.size; if (isSymbolicLink) { linkCount++; if (this.options.includeLink) { totalSize = totalSize.plus(fileSize.toString()); } } else { totalSize = totalSize.plus(fileSize.toString()); } if (isDirectory) { // 检查文件夹深度限制,只在递归进入子目录前检查 if (depth > this.options.maxDepth) { return; } if (itemPath !== folderPath) { // 排除当前文件夹 directoryCount++; } let entries; try { // 读取目录内容 entries = await fs.promises.readdir(itemPath); } catch (error) { return this.handleError(error, `无法读取目录: ${error.message}`, itemPath); } // 简化的并发控制 const semaphore = new SimpleSemaphore(this.options.concurrency); await Promise.all(entries.map(async (entry) => { await semaphore.acquire(); try { const childPath = path$1.join(itemPath, entry); await processItem(childPath, depth + 1); } catch (error) { throw error; } finally { semaphore.release(); } })); } else if (isFile) { fileCount++; } }; await processItem(folderPath); return { size: totalSize, fileCount, directoryCount, linkCount }; } /** * 处理错误 * @param error 错误对象 * @param message 错误消息 * @param path 错误路径 */ handleError(error, message, path) { const errorRes = { error, message, path }; // 如果设置了错误回调,调用它 if (this.options.onError) { const shouldContinue = this.options.onError(errorRes); if (!shouldContinue) { throw new Error(`计算被用户停止: ${message}`); } } if (!this.options.ignoreErrors) { throw new Error(message); } } } /** * 文件系统工具类 * 提供常用的文件系统操作辅助功能 */ class FileSystemUtils { /** * 格式化文件大小为人类可读的字符串 * @param bytes 字节大小 * @param decimals 小数位数 * @returns 格式化后的大小字符串 */ static formatFileSize(bytes, decimals = 2) { if (!bytes) { return '0 Bytes'; } if (typeof bytes === 'string') { if (bytes.length === 0 || bytes.trim().length === 0) { return '0 Bytes'; } } const size = new bignumber_js.BigNumber(bytes); // 如果大小为0,直接返回 if (size.isZero()) { return '0 Bytes'; } const k = new bignumber_js.BigNumber(1024); const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; // 计算单位级别 let i = 0; let tempSize = size; while (tempSize.gte(k) && i < sizes.length - 1) { tempSize = tempSize.div(k); i++; } // 计算最终结果 const result = size.div(k.pow(i)); return result.toFixed(dm) + ' ' + sizes[i]; } /** * 获取路径的相对路径 * @param basePath 基础路径 * @param targetPath 目标路径 * @returns 相对路径 */ static getRelativePath(basePath, targetPath) { const normalize = (path) => path.replace(/[\\\/]+/g, '/'); const base = normalize(basePath); const target = normalize(targetPath); if (target.startsWith(base)) { const relative = target.substring(base.length); return relative.startsWith('/') ? relative.substring(1) : relative; } return target; } /** * 验证路径是否安全(防止路径遍历攻击) * @param path 要验证的路径 * @returns 是否安全 */ static isPathSafe(path) { // 检查路径遍历攻击模式 const dangerousPatterns = [ /\.\.\//, // ../ /\.\.\\/, // ..\ /^\/+/, // 绝对路径 /^[a-zA-Z]:\\/ // Windows 绝对路径 ]; return !dangerousPatterns.some(pattern => pattern.test(path)); } } exports.FileSystemUtils = FileSystemUtils; exports.FolderSize = FolderSize; exports.SimpleSemaphore = SimpleSemaphore;