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
text/typescript
/**
* 同步文件操作工具(与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
};