# fs-syn **Repository Path**: nodets/fs-syn ## Basic Information - **Project Name**: fs-syn - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-10-06 - **Last Updated**: 2025-10-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # fs-syn 一个轻量级的**纯同步** Node.js 文件操作工具,基于原生 `fs` 模块构建,**零依赖**。它简化了常见的文件和目录操作,并提供清晰的 TypeScript 类型提示,专为需要同步操作的场景设计。 ## 功能特性 - 🚫 **纯同步操作**:所有操作都是同步的,无需 async/await 或 Promise - 📦 **零依赖**:完全基于 Node.js 原生 `fs` 模块,无外部库依赖 - 🔧 **核心操作**:涵盖文件 I/O、目录管理、路径匹配和哈希计算 - ✅ **TypeScript 支持**:完整的类型定义,提供类型安全和 IDE 自动补全 - 🖥️ **跨平台**:在 Windows、macOS 和 Linux 上统一处理路径分隔符 ## 安装 ### 安装核心包 ```bash npm install fs-syn --save ``` ### TypeScript 类型支持(Node.js < 16) 对于 Node.js 16 以下版本,安装 @types/node 以获得完整的 TypeScript 类型提示: ```bash npm install @types/node --save-dev ``` ## 快速开始 ```typescript import fs from 'fs-syn'; try { // 1. 创建目录(如需要会递归创建) fs.mkdir('./example'); // 2. 写入文件内容(自动创建父目录) fs.write('./example/hello.txt', 'Hello World!'); // 3. 读取文件内容 const content = fs.read('./example/hello.txt'); console.log(content); // 输出: Hello World! // 4. 清理(删除目录及其内容) fs.remove('./example'); } catch (err: any) { console.error('操作失败:', err.message); } ``` ## 完整 API 示例 所有方法在失败时都会抛出描述性错误(如文件不存在、权限问题等)。使用 `try/catch` 块来优雅地处理错误。 ### 1. 文件操作 #### `write(file: string, content: string | Buffer, options?: fs.WriteFileOptions)` 将内容写入文件(如果目录不存在会自动创建)。 ```typescript // 将字符串写入日志文件 fs.write('./logs/access.log', '用户在上午10:00登录'); // 将二进制数据(Buffer)写入文件 const binaryData = Buffer.from('原始二进制内容', 'utf8'); fs.write('./data/bin.dat', binaryData); // 使用自定义选项写入(例如追加模式、特定编码) fs.write('./config.ini', 'debug=true', { encoding: 'utf8', flag: 'a' // 追加到文件而不是覆盖 }); ``` #### `read(file: string, options?: { encoding?: BufferEncoding | null; flag?: string })` 从文件读取内容(默认返回字符串,如果 encoding: null 则返回 Buffer)。 ```typescript // 以 UTF-8 字符串形式读取文件(默认) const text = fs.read('./notes.txt'); console.log('文件内容:', text); // 以 Buffer 形式读取文件(适用于二进制文件如图片) const imageBuffer = fs.read('./assets/logo.png', { encoding: null }); // 使用自定义标志读取(例如只读模式) const lockedContent = fs.read('./protected.txt', { flag: 'r' }); ``` #### `readJSON(file: string, options?: { encoding?: BufferEncoding | null; flag?: string })` 直接读取并解析 JSON 文件(为 TypeScript 用户返回类型化对象)。 ```typescript // 为类型安全定义 TypeScript 接口 interface AppConfig { port: number; debug: boolean; theme: string; } // 读取并解析带类型提示的 JSON const config = fs.readJSON('./config.json'); console.log('服务器端口:', config.port); // 类型检查: config.port 是 number // 使用自定义编码读取(例如 ASCII) const legacyData = fs.readJSON('./legacy-data.json', { encoding: 'ascii' }); ``` #### `writeJSON(file: string, data: unknown, spaces?: number)` 将 JavaScript 对象写入 JSON 文件并自动格式化。 ```typescript // 将用户对象写入 JSON(默认 2 空格缩进) const user = { id: 123, name: '张三', roles: ['编辑者', '管理员'] }; fs.writeJSON('./users/zhang.json', user); // 使用 4 空格缩进写入以提高可读性 fs.writeJSON('./config.json', { theme: 'dark', timeout: 5000 }, 4); ``` #### `appendFile(file: string, content: string | Buffer, options?: fs.WriteFileOptions)` 向现有文件追加内容(如果文件不存在则创建)。 ```typescript // 追加带时间戳的日志条目 const logEntry = `[INFO] 服务器在 ${new Date().toISOString()} 重启\n`; fs.appendFile('./app.log', logEntry); // 向日志文件追加二进制数据 const sensorData = Buffer.from([0x01, 0x0A, 0xFF]); // 示例传感器值 fs.appendFile('./sensor/log.dat', sensorData); ``` #### `copy(from: string, to: string, options?: { force?: boolean; preserveTimestamps?: boolean })` 递归复制文件或目录(支持覆盖和保留文件时间戳)。 ```typescript // 复制单个文件到新位置 fs.copy('./docs/manual.pdf', './public/downloads/user-manual.pdf'); // 复制整个目录(如果目标存在则强制覆盖) fs.copy('./src', './src-backup', { force: true }); // 复制时保留时间戳(保留原始创建/修改时间) fs.copy('./archive/2023', './backup/2023', { preserveTimestamps: true }); ``` #### `move(from: string, to: string, options?: { force?: boolean })` 移动或重命名文件/目录(对于跨设备移动会降级为"复制+删除")。 ```typescript // 重命名文件(同一目录内) fs.move('./tmp/report-draft.txt', './reports/final-report.txt'); // 将目录移动到新的父文件夹 fs.move('./old-docs', './archive/2024-docs'); // 强制覆盖现有的目标文件 fs.move('./new-config.json', './current-config.json', { force: true }); ``` #### `remove(path: string)` 递归删除文件或目录(相当于 Unix 上的 `rm -rf` 或 Windows 上的 `rmdir /s /q`)。 ```typescript // 删除单个文件 fs.remove('./temp.log'); // 删除整个目录(及其所有内容) fs.remove('./node_modules'); // 删除嵌套子目录 fs.remove('./dist/assets/old-images'); ``` #### `hashFile(file: string, algorithm?: string)` 计算文件的加密哈希(支持 Node.js 的 `crypto` 模块提供的所有算法)。 ```typescript // 计算 SHA-256 哈希(默认算法) const sha256Hash = fs.hashFile('./downloads/installer.exe'); console.log('SHA-256:', sha256Hash); // 计算 MD5 哈希(常用于文件完整性检查) const md5Hash = fs.hashFile('./docs.pdf', 'md5'); console.log('MD5:', md5Hash); // 计算 SHA-1 哈希(遗留用途) const sha1Hash = fs.hashFile('./legacy-data.bin', 'sha1'); ``` ### 2. 目录操作 #### `mkdir(dirPath: string)` 创建目录(以及所有父目录)递归(相当于 Unix 上的 `mkdir -p`)。 ```typescript // 创建单个目录 fs.mkdir('./new-folder'); // 创建嵌套目录(无需先创建父目录) fs.mkdir('./src/assets/images/icons'); // 创建名称包含空格的目录 fs.mkdir('./docs/user guides'); ``` #### `readDir(dir: string, options?: { withFileTypes?: boolean })` 读取目录内容(默认返回文件名数组,或返回 fs.Dirent 对象以获取类型信息)。 ```typescript // 将目录内容读取为文件名数组 const files = fs.readDir('./src'); console.log('src 中的文件:', files); // 例如: ["index.ts", "utils/"] // 读取包含文件类型信息的内容(区分文件和目录) const entries = fs.readDir('./docs', { withFileTypes: true }); entries.forEach(entry => { if (entry.isDirectory()) { console.log(`目录: ${entry.name}`); } else { console.log(`文件: ${entry.name}`); } }); ``` #### `emptyDir(dir: string)` 清空目录内容**但不删除目录本身**。 ```typescript // 清空日志目录(保留 ./logs 文件夹) fs.emptyDir('./logs'); // 清空缓存目录(保留 ./node_modules/.cache 文件夹) fs.emptyDir('./node_modules/.cache'); ``` #### `isDir(path: string)` 检查给定路径是否指向现有目录。 ```typescript // 检查目录是否存在 if (fs.isDir('./src')) { console.log('./src 是一个有效目录'); } // 动态路径检查 const targetPath = './unknown-folder'; console.log(`${targetPath} ${fs.isDir(targetPath) ? '是' : '不是'}目录`); ``` #### `isFile(path: string)` 检查给定路径是否指向现有文件。 ```typescript // 检查文件是否存在且不是目录 if (fs.isFile('./package.json')) { console.log('package.json 存在(且是文件)'); } // 条件性处理文件 const dataFile = './data.csv'; if (fs.isFile(dataFile)) { const content = fs.read(dataFile); // 处理文件内容... } ``` #### `exists(...paths: string[])` 检查路径是否存在(支持多个路径段来构建完整路径)。 ```typescript // 检查单个路径是否存在 if (fs.exists('./config.json')) { console.log('找到配置文件'); } // 从多个段构建并检查路径(避免手动连接) if (fs.exists('src', 'utils', 'helpers.ts')) { console.log('helpers.ts 存在于 src/utils/ 中'); } ``` ### 3. 路径工具 #### [expand(patterns: string | string[], options?: ExpandOptions)] 使用类似 glob 的模式匹配路径(支持 `*` 进行单级匹配和 `**` 进行多级匹配)。 **`ExpandOptions` 接口**: - `cwd?: string`: 搜索的工作目录(默认: `process.cwd()`)。 - `dot?: boolean`: 包含隐藏文件/目录(以 . 开头,默认: `false`)。 - `onlyFiles?: boolean`: 仅返回文件(排除目录,默认: `false`)。 - `onlyDirs?: boolean`: 仅返回目录(排除文件,默认: `false`)。 ```typescript // 查找项目中所有 TypeScript 文件(递归) const tsFiles = fs.expand('**/*.ts'); console.log('TypeScript 文件:', tsFiles); // 例如: ["src/index.ts", "tests/utils.ts"] // 查找 ./src 目录中的所有 JSON 文件(非递归) const srcJsonFiles = fs.expand('./src/*.json'); // 查找匹配 "docs-*" 的目录(例如 docs-v1, docs-v2) const docDirs = fs.expand('docs-*', { onlyDirs: true }); // 在 ./config 目录中包含隐藏文件(例如 .env, .gitignore) const hiddenConfigFiles = fs.expand('*', { cwd: './config', dot: true, onlyFiles: true }); ``` #### `isPathAbsolute(path: string)` 检查路径是否为绝对路径(跨平台工作)。 ```typescript // POSIX 系统(macOS/Linux) console.log(fs.isPathAbsolute('/usr/local/bin')); // true console.log(fs.isPathAbsolute('./relative/path')); // false // Windows 系统 console.log(fs.isPathAbsolute('C:\\Windows\\System32')); // true console.log(fs.isPathAbsolute('..\\relative\\path')); // false ``` #### [doesPathContain(ancestor: string, ...paths: string[])] 检查一个或多个"子"路径是否包含在"祖先"目录中(防止路径遍历问题)。 ```typescript // 检查多个文件是否在 ./src 目录内 const allInSrc = fs.doesPathContain( './src', './src/index.ts', './src/utils/helpers.ts' ); console.log('所有文件都在 src 中:', allInSrc); // true // 检查日志是否包含在 /var/log 中(POSIX) const logsInVar = fs.doesPathContain('/var/log', '/var/log/app.log', '/var/log/nginx'); ``` #### `createSymlink(target: string, linkPath: string, options?: { type?: 'file' | 'dir' | 'junction' })` 创建指向文件或目录的符号链接(symlink)。 ```typescript // 创建指向文件的符号链接(指向 ./dist/index.js) fs.createSymlink('./dist/index.js', './current.js', { type: 'file' }); // 创建指向目录的符号链接(指向 ./docs/latest) fs.createSymlink('./docs/latest', './docs/current', { type: 'dir' }); // 创建连接点(仅 Windows)用于网络共享 if (process.platform === 'win32') { fs.createSymlink('\\\\server\\data', './network-data', { type: 'junction' }); } ``` #### `realpath(path: string)` 将符号链接解析为其**真实的物理路径**(跟随嵌套符号链接)。 ```typescript // 解析符号链接到其目标 const realFilePath = fs.realpath('./current.js'); console.log('符号链接指向:', realFilePath); // 例如: "./dist/index.js" // 解析嵌套符号链接 const resolvedPath = fs.realpath('./deep/link/to/file'); ``` ## 错误处理 所有方法都会抛出带有可读消息的 `Error` 对象。使用 `try/catch` 处理特定的失败场景: ```typescript try { // 尝试读取不存在的文件 fs.read('./nonexistent-file.txt'); } catch (err: any) { console.error('错误名称:', err.name); // "Error" console.error('错误消息:', err.message); // "文件不存在: ./nonexistent-file.txt" console.error('受影响路径:', err.path); // (可选) 导致错误的路径 } ``` ### 常见错误场景 | 错误消息 | 原因 | 解决方法 | |----------|------|----------| | `文件不存在: [path]` | 目标文件不存在 | 验证路径,或先创建文件 | | `目录不存在: [path]` | 目标目录不存在 | 使用 `fs.mkdir` 先创建目录 | | `目标已存在(设 force: true 覆盖): [path]` | `copy`/`move` 的目标路径已存在 | 在选项中添加 `force: true` 以覆盖 | | `路径是目录: [path]` | 你试图将目录当作文件使用(例如 `fs.read('./src')`) | 验证路径指向文件而不是目录 | ## 重要说明 ### 1. 同步操作会阻塞事件循环 同步 I/O 会阻塞 Node.js 的事件循环直到操作完成。**仅在以下场景使用 `fs-syn`**: - 简单脚本、CLI 工具或构建过程 - 配置文件加载(在启动时,在服务请求之前) - 小到中等大小的文件处理(避免导致长时间延迟的大文件) **避免**在高性能服务器应用(如 Express/Koa API)中使用 `fs-syn`,在这些场景中非阻塞 I/O 至关重要。 ### 2. `remove` 操作具有破坏性 `fs.remove` 方法会递归删除文件和目录(类似 `rm -rf`)。**始终仔细检查路径**以防止意外数据丢失(例如,永远不要使用 `fs.remove('/')` 或 `fs.remove('./')`)。 ### 3. 模式匹配限制 `expand` 方法支持基本的 glob 模式(`*` 和 `**`),但不如专用库如 `glob` 强大(例如,不支持否定模式如 `!exclude-me.ts`)。对于复杂匹配,考虑将 `fs-syn` 与轻量级 glob 库结合使用。 ## 兼容性 - **Node.js**: 14.0.0 或更高版本(支持完整的 `fs` 模块同步 API) - **TypeScript**: 4.5 或更高版本(用于类型定义) - **操作系统**: Windows, macOS, Linux ## 许可证 MIT 许可证。详见 [LICENSE](https://github.com/your-username/fs-syn/blob/main/LICENSE)。 ```