UNPKG

fs-syn

Version:

Lightweight purely synchronous file operation utility for Node.js, built on native fs module with zero dependencies

444 lines (381 loc) 10.6 kB
/** * 同步文件操作工具(与fs-async对应的同步版本) * 基于Node.js原生fs模块的同步方法实现,无第三方依赖 */ import * as fs from 'fs'; import * as path from 'path'; import { createHash } from 'crypto'; // 选项接口定义 export interface CopyOptions { force?: boolean; preserveTimestamps?: boolean; } export interface ExpandOptions { cwd?: string; dot?: boolean; onlyFiles?: boolean; onlyDirs?: boolean; } export interface MoveOptions { force?: boolean; } export interface SymlinkOptions { type?: 'file' | 'dir' | 'junction'; } // 全局配置 export let defaultEncoding: BufferEncoding = 'utf-8'; // 内部工具函数 function pathExists(p: string): boolean { try { fs.accessSync(p); return true; } catch { return false; } } function getStat(p: string): fs.Stats { try { return fs.statSync(p); } catch (err: any) { throw new Error(`获取路径状态失败:${err.message}(路径:${p})`); } } function mkdirRecursive(p: string): void { if (pathExists(p)) return; mkdirRecursive(path.dirname(p)); try { fs.mkdirSync(p); } catch (err: any) { throw new Error(`创建目录失败:${err.message}(路径:${p})`); } } function toAbsolutePath(p: string): string { return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p); } /** * 简易glob模式匹配(替代glob模块) * 支持 * 和 **模式 */ function matchPattern(filePath: string, pattern: string): boolean { // 转换为统一的路径分隔符 const normalizedPath = filePath.replace(/\\/g, '/'); const normalizedPattern = pattern.replace(/\\/g, '/'); // 处理**模式 if (normalizedPattern.includes('**')) { const parts = normalizedPattern.split('**'); if (parts.length > 2) return false; // 不支持多个** const [prefix, suffix] = parts; const prefixMatch = prefix === '' || normalizedPath.startsWith(prefix); const suffixMatch = suffix === '' || normalizedPath.endsWith(suffix); return prefixMatch && suffixMatch; } // 处理*模式 const regexPattern = normalizedPattern .replace(/\./g, '\\.') .replace(/\*/g, '[^/]*') .replace(/\?/g, '.'); const regex = new RegExp(`^${regexPattern}$`); return regex.test(normalizedPath); } /** * 递归收集目录下所有文件和目录 */ function collectPaths( dir: string, currentPath: string = '', includeDot: boolean = false ): string[] { const fullDir = currentPath ? path.join(dir, currentPath) : dir; const entries = fs.readdirSync(fullDir, { withFileTypes: true }); const paths: string[] = []; for (const entry of entries) { // 过滤隐藏文件 if (!includeDot && entry.name.startsWith('.')) continue; const entryPath = currentPath ? path.join(currentPath, entry.name) : entry.name; if (entry.isDirectory()) { paths.push(entryPath); // 递归收集子目录 const subPaths = collectPaths(dir, entryPath, includeDot); paths.push(...subPaths); } else { paths.push(entryPath); } } return paths; } // 核心方法 export function copy( from: string, to: string, options: CopyOptions = {} ): void { const { force = false, preserveTimestamps = false } = options; if (!pathExists(from)) throw new Error(`源路径不存在:${from}`); const fromStat = getStat(from); const isFile = fromStat.isFile(); const isDir = fromStat.isDirectory(); if (pathExists(to)) { if (!force) throw new Error(`目标已存在(设 force: true 覆盖):${to}`); remove(to); } if (isFile) { mkdirRecursive(path.dirname(to)); fs.copyFileSync( from, to, preserveTimestamps ? fs.constants.COPYFILE_FICLONE : 0 ); return; } if (isDir) { mkdirRecursive(to); const files = fs.readdirSync(from); for (const file of files) { copy(path.join(from, file), path.join(to, file), options); } if (preserveTimestamps) { fs.utimesSync(to, fromStat.atime, fromStat.mtime); } } } export function move( from: string, to: string, options: MoveOptions = {} ): void { const { force = false } = options; if (!pathExists(from)) throw new Error(`源路径不存在:${from}`); if (pathExists(to) && !force) throw new Error(`目标已存在:${to}`); mkdirRecursive(path.dirname(to)); try { fs.renameSync(from, to); } catch (err: any) { if (err.code === 'EXDEV') { // 跨设备移动,降级为复制+删除 copy(from, to, { force: true, preserveTimestamps: true }); remove(from); } else { throw new Error(`移动失败:${err.message}`); } } } export function mkdir(dirPath: string): void { mkdirRecursive(dirPath); } /** * 原生实现的路径匹配(替代glob) * 支持基本的*和**模式 */ export function expand( patterns: string | string[], options: ExpandOptions = {} ): string[] { const { cwd = process.cwd(), dot = false, onlyFiles = false, onlyDirs = false } = options; if (onlyFiles && onlyDirs) { throw new Error("onlyFiles 和 onlyDirs 不能同时为 true"); } const patternList = Array.isArray(patterns) ? patterns : [patterns]; const allPaths = collectPaths(cwd, '', dot); // 过滤匹配模式的路径 const matchedPaths: string[] = []; for (const pathStr of allPaths) { if (patternList.some(pattern => matchPattern(pathStr, pattern))) { matchedPaths.push(pathStr); } } // 过滤文件/目录类型 if (onlyFiles || onlyDirs) { const filtered: string[] = []; for (const pathItem of matchedPaths) { const fullPath = path.join(cwd, pathItem); if (isDir(fullPath) && onlyDirs) { filtered.push(pathItem); } else if (isFile(fullPath) && onlyFiles) { filtered.push(pathItem); } } return filtered; } return matchedPaths; } export function write( file: string, content: string | Buffer, options: fs.WriteFileOptions = {} ): void { // 使用Object.assign替代扩展操作符解决类型错误 const opts: fs.WriteFileOptions = Object.assign( { encoding: defaultEncoding }, options ); mkdirRecursive(path.dirname(file)); fs.writeFileSync(file, content, opts); } export function read( file: string, options: { encoding?: BufferEncoding; flag?: string } = {} ): string | Buffer { if (!pathExists(file)) throw new Error(`文件不存在:${file}`); if (isDir(file)) throw new Error(`路径是目录:${file}`); const opts = Object.assign({ encoding: defaultEncoding }, options); return fs.readFileSync(file, opts); } export function readJSON<T = any>( file: string, options: { encoding?: BufferEncoding; flag?: string } = {} ): T { const content = read(file, options); const jsonStr = typeof content === 'string' ? content : content.toString(); try { return JSON.parse(jsonStr) as T; } catch (err: any) { throw new Error(`JSON解析失败:${err.message}`); } } export function writeJSON( file: string, data: any, spaces: number = 2 ): void { write(file, JSON.stringify(data, null, spaces), { encoding: 'utf8' }); } export function remove(p: string): void { if (!pathExists(p)) return; const stat = getStat(p); if (stat.isFile() || stat.isSymbolicLink()) { fs.unlinkSync(p); return; } if (stat.isDirectory()) { const files = fs.readdirSync(p); for (const file of files) { remove(path.join(p, file)); } fs.rmdirSync(p); } } export function exists(...paths: string[]): boolean { if (paths.length === 0) throw new Error('请传入路径参数'); return pathExists(path.join(...paths)); } export function isDir(p: string): boolean { return pathExists(p) && getStat(p).isDirectory(); } export function isFile(p: string): boolean { return pathExists(p) && getStat(p).isFile(); } export function isLink(p: string): boolean { return pathExists(p) && getStat(p).isSymbolicLink(); } export function isPathAbsolute(p: string): boolean { return path.isAbsolute(p); } export function doesPathContain( ancestor: string, ...paths: string[] ): boolean { const ancestorAbs = toAbsolutePath(ancestor); const ancestorWithSep = path.join(ancestorAbs, path.sep); for (const p of paths) { const pAbs = toAbsolutePath(p); if (!pAbs.startsWith(ancestorWithSep) && pAbs !== ancestorAbs) { return false; } } return true; } // 扩展方法 export function readDir( dir: string, options?: { withFileTypes?: boolean } ): string[] | fs.Dirent[] { if (!isDir(dir)) throw new Error(`不是目录:${dir}`); if (options?.withFileTypes) { return fs.readdirSync(dir, { withFileTypes: true }) as fs.Dirent[]; } return fs.readdirSync(dir) as string[]; } export function stat(p: string): fs.Stats { return getStat(p); } export function chmod(p: string, mode: number | string): void { fs.chmodSync(p, mode); } export function createSymlink( target: string, linkPath: string, options: SymlinkOptions = {} ): void { mkdirRecursive(path.dirname(linkPath)); fs.symlinkSync(target, linkPath, options.type); } export function realpath(p: string): string { return fs.realpathSync(p); } export function emptyDir(dir: string): void { if (!isDir(dir)) throw new Error(`不是目录:${dir}`); const files = fs.readdirSync(dir); for (const file of files) { remove(path.join(dir, file)); } } export function ensureFile(file: string): void { if (pathExists(file)) { if (isDir(file)) throw new Error(`路径是目录:${file}`); return; } mkdirRecursive(path.dirname(file)); fs.writeFileSync(file, '', 'utf8'); } export function appendFile( file: string, content: string | Buffer, options: fs.WriteFileOptions = {} ): void { const opts: fs.WriteFileOptions = Object.assign( { encoding: defaultEncoding }, options ); mkdirRecursive(path.dirname(file)); fs.appendFileSync(file, content, opts); } export function hashFile( file: string, algorithm: string = 'sha256' ): string { if (!isFile(file)) throw new Error(`不是文件:${file}`); return createHash(algorithm).update(fs.readFileSync(file)).digest('hex'); } // 导出默认对象 export default { defaultEncoding, copy, move, mkdir, expand, write, read, readJSON, writeJSON, remove, exists, isDir, isFile, isLink, isPathAbsolute, doesPathContain, readDir, stat, chmod, createSymlink, realpath, emptyDir, ensureFile, appendFile, hashFile };