406 lines
11 KiB
JavaScript
406 lines
11 KiB
JavaScript
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
const { defaultLogger } = require('./logger');
|
|
|
|
// 创建logger实例
|
|
const logger = defaultLogger.child('FileUtils');
|
|
|
|
|
|
/**
|
|
* 文件操作工具类 - 确保与 pkg 打包兼容
|
|
*/
|
|
class FileUtils {
|
|
/**
|
|
* 安全删除文件(兼容 pkg 打包,增强Windows权限处理)
|
|
*/
|
|
static async safeDeleteFile(filePath) {
|
|
try {
|
|
// 首先检查文件是否存在
|
|
if (!(await fs.pathExists(filePath))) {
|
|
logger.debug(`文件不存在,无需删除: ${filePath}`);
|
|
return true;
|
|
}
|
|
|
|
// 在 Windows 上进行更全面的文件占用检查
|
|
if (process.platform === 'win32') {
|
|
const isOccupied = await this.isFileOccupied(filePath);
|
|
if (isOccupied) {
|
|
logger.debug(`文件被占用,跳过删除: ${filePath}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 尝试删除文件
|
|
await fs.remove(filePath);
|
|
logger.debug(`文件删除成功: ${filePath}`);
|
|
return true;
|
|
} catch (error) {
|
|
// 如果是权限错误,尝试Windows特定的删除方法
|
|
if (error.code === 'EPERM' || error.code === 'EACCES') {
|
|
if (process.platform === 'win32') {
|
|
return await this.forceDeleteFileWindows(filePath);
|
|
} else {
|
|
logger.warn(`删除文件权限不足: ${filePath}`, error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 其他错误类型
|
|
if (error.code === 'ENOENT') {
|
|
logger.debug(`文件不存在,删除成功: ${filePath}`);
|
|
return true;
|
|
}
|
|
|
|
if (error.code === 'EBUSY') {
|
|
logger.debug(`文件被占用,删除失败: ${filePath}`);
|
|
return false;
|
|
}
|
|
|
|
logger.warn(`删除文件失败: ${filePath}`, error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查文件是否被占用(Windows专用)
|
|
*/
|
|
static async isFileOccupied(filePath) {
|
|
try {
|
|
const nativeFs = require('fs').promises;
|
|
|
|
// 尝试以独占模式打开文件
|
|
const handle = await nativeFs.open(filePath, 'r+');
|
|
await handle.close();
|
|
return false; // 文件未被占用
|
|
} catch (error) {
|
|
if (error.code === 'EBUSY' || error.code === 'EPERM' || error.code === 'EACCES') {
|
|
return true; // 文件被占用
|
|
}
|
|
// 其他错误认为文件未被占用
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 强制删除文件(Windows专用)
|
|
*/
|
|
static async forceDeleteFileWindows(filePath) {
|
|
try {
|
|
const nativeFs = require('fs').promises;
|
|
|
|
// 尝试修改文件属性
|
|
try {
|
|
await nativeFs.chmod(filePath, 0o666);
|
|
logger.debug(`修改文件权限成功: ${filePath}`);
|
|
} catch (chmodError) {
|
|
logger.debug(`修改文件权限失败: ${filePath}`, chmodError.message);
|
|
}
|
|
|
|
// 短暂等待,让系统释放文件句柄
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
// 再次尝试删除
|
|
await nativeFs.unlink(filePath);
|
|
logger.info(`强制删除成功: ${filePath}`);
|
|
return true;
|
|
} catch (forceError) {
|
|
if (forceError.code === 'ENOENT') {
|
|
logger.debug(`强制删除时文件不存在: ${filePath}`);
|
|
return true;
|
|
}
|
|
|
|
logger.warn(`强制删除失败: ${filePath}`, forceError.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安全创建目录(兼容 pkg 打包)
|
|
*/
|
|
static async safeEnsureDir(dirPath) {
|
|
try {
|
|
// 首先尝试使用 fs-extra
|
|
await fs.ensureDir(dirPath);
|
|
return true;
|
|
} catch (error) {
|
|
try {
|
|
// 降级到原生 fs
|
|
const nativeFs = require('fs').promises;
|
|
await nativeFs.mkdir(dirPath, { recursive: true });
|
|
return true;
|
|
} catch (nativeError) {
|
|
logger.error(`目录创建失败: ${dirPath}`, nativeError.message);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 增强的目录创建方法(专门处理打包后的权限问题)
|
|
*/
|
|
static async safeEnsureDirEnhanced(dirPath) {
|
|
try {
|
|
// 规范化路径
|
|
const normalizedPath = path.resolve(dirPath);
|
|
|
|
// 检查是否在打包环境中
|
|
const isPkg = process.pkg !== undefined;
|
|
|
|
if (isPkg) {
|
|
// 在打包环境中,使用更保守的方法
|
|
return await this.createDirectoryRecursive(normalizedPath);
|
|
} else {
|
|
// 在开发环境中,使用标准方法
|
|
await fs.ensureDir(normalizedPath);
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
logger.error(`增强目录创建失败: ${dirPath}`, error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 递归创建目录(处理权限问题)
|
|
*/
|
|
static async createDirectoryRecursive(dirPath) {
|
|
try {
|
|
const parts = dirPath.split(path.sep);
|
|
let currentPath = '';
|
|
|
|
for (const part of parts) {
|
|
if (!part) continue;
|
|
|
|
currentPath = currentPath ? path.join(currentPath, part) : part;
|
|
|
|
try {
|
|
// 检查目录是否已存在
|
|
const stats = await fs.stat(currentPath);
|
|
if (!stats.isDirectory()) {
|
|
throw new Error(`路径存在但不是目录: ${currentPath}`);
|
|
}
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
// 目录不存在,尝试创建
|
|
try {
|
|
await fs.mkdir(currentPath);
|
|
// 验证创建是否成功
|
|
await fs.access(currentPath);
|
|
} catch (mkdirError) {
|
|
logger.error(`创建目录失败: ${currentPath}`, mkdirError.message);
|
|
throw mkdirError;
|
|
}
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`递归创建目录失败: ${dirPath}`, error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安全写入文件(处理权限问题)
|
|
*/
|
|
static async safeWriteFile(filePath, data, options = {}) {
|
|
try {
|
|
// 确保目录存在
|
|
const dirPath = path.dirname(filePath);
|
|
const dirCreated = await this.safeEnsureDirEnhanced(dirPath);
|
|
|
|
if (!dirCreated) {
|
|
throw new Error(`无法创建目录: ${dirPath}`);
|
|
}
|
|
|
|
// 检查文件是否被占用
|
|
if (await fs.pathExists(filePath)) {
|
|
try {
|
|
// 尝试删除现有文件
|
|
await fs.remove(filePath);
|
|
} catch (removeError) {
|
|
logger.warn(`删除现有文件失败: ${filePath}`, removeError.message);
|
|
// 继续尝试写入,可能会覆盖
|
|
}
|
|
}
|
|
|
|
// 写入文件
|
|
await fs.writeFile(filePath, data, options);
|
|
|
|
// 验证写入是否成功
|
|
await fs.access(filePath);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`安全写入文件失败: ${filePath}`, error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安全创建写入流(处理权限问题)
|
|
*/
|
|
static async safeCreateWriteStream(filePath) {
|
|
try {
|
|
// 确保目录存在
|
|
const dirPath = path.dirname(filePath);
|
|
const dirCreated = await this.safeEnsureDirEnhanced(dirPath);
|
|
|
|
if (!dirCreated) {
|
|
throw new Error(`无法创建目录: ${dirPath}`);
|
|
}
|
|
|
|
// 检查文件是否被占用
|
|
if (await fs.pathExists(filePath)) {
|
|
try {
|
|
await fs.remove(filePath);
|
|
} catch (removeError) {
|
|
logger.warn(`删除现有文件失败: ${filePath}`, removeError.message);
|
|
}
|
|
}
|
|
|
|
// 创建写入流
|
|
return fs.createWriteStream(filePath);
|
|
} catch (error) {
|
|
logger.error(`创建写入流失败: ${filePath}`, error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安全检查文件是否存在(兼容 pkg 打包)
|
|
*/
|
|
static async safePathExists(filePath) {
|
|
try {
|
|
// 首先尝试使用 fs-extra
|
|
return await fs.pathExists(filePath);
|
|
} catch (error) {
|
|
try {
|
|
// 降级到原生 fs
|
|
const nativeFs = require('fs').promises;
|
|
await nativeFs.access(filePath);
|
|
return true;
|
|
} catch (nativeError) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安全读取目录(兼容 pkg 打包)
|
|
*/
|
|
static async safeReadDir(dirPath) {
|
|
try {
|
|
// 首先尝试使用 fs-extra
|
|
return await fs.readdir(dirPath);
|
|
} catch (error) {
|
|
try {
|
|
// 降级到原生 fs
|
|
const nativeFs = require('fs').promises;
|
|
return await nativeFs.readdir(dirPath);
|
|
} catch (nativeError) {
|
|
logger.error(`读取目录失败: ${dirPath}`, nativeError.message);
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安全写入 JSON 文件(兼容 pkg 打包)
|
|
*/
|
|
static async safeWriteJson(filePath, data, options = {}) {
|
|
try {
|
|
// 首先尝试使用 fs-extra
|
|
await fs.writeJson(filePath, data, options);
|
|
return true;
|
|
} catch (error) {
|
|
try {
|
|
// 降级到原生 fs
|
|
const nativeFs = require('fs').promises;
|
|
const jsonString = JSON.stringify(data, null, options.spaces || 2);
|
|
await nativeFs.writeFile(filePath, jsonString, 'utf8');
|
|
return true;
|
|
} catch (nativeError) {
|
|
logger.error(`JSON 写入失败: ${filePath}`, nativeError.message);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检测是否在 pkg 打包环境中运行
|
|
*/
|
|
static isPkgEnvironment() {
|
|
return process.pkg !== undefined;
|
|
}
|
|
|
|
/**
|
|
* 获取当前运行环境信息
|
|
*/
|
|
static getEnvironmentInfo() {
|
|
return {
|
|
isPkg: this.isPkgEnvironment(),
|
|
nodeVersion: process.version,
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
pkgVersion: process.pkg ? process.pkg.version : null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 检查文件权限
|
|
*/
|
|
static async checkFilePermissions(filePath, mode = 'w') {
|
|
try {
|
|
const nativeFs = require('fs').promises;
|
|
await nativeFs.access(filePath, mode === 'w' ? nativeFs.constants.W_OK : nativeFs.constants.R_OK);
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查目录权限
|
|
*/
|
|
static async checkDirectoryPermissions(dirPath, mode = 'w') {
|
|
try {
|
|
const nativeFs = require('fs').promises;
|
|
await nativeFs.access(dirPath, mode === 'w' ? nativeFs.constants.W_OK : nativeFs.constants.R_OK);
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 等待文件释放(处理文件占用问题)
|
|
*/
|
|
static async waitForFileRelease(filePath, maxWaitTime = 5000) {
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < maxWaitTime) {
|
|
try {
|
|
const nativeFs = require('fs').promises;
|
|
await nativeFs.access(filePath, nativeFs.constants.W_OK);
|
|
return true;
|
|
} catch (error) {
|
|
if (error.code === 'EBUSY' || error.code === 'EACCES') {
|
|
// 文件被占用或无权限,等待一段时间后重试
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
continue;
|
|
}
|
|
// 其他错误,直接返回
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
module.exports = FileUtils;
|