修复缓存删除问题
This commit is contained in:
+256
-50
@@ -32,6 +32,9 @@ class ApiCacheService {
|
||||
// 创建配置管理器
|
||||
this.configManager = new CacheConfigManager();
|
||||
|
||||
// 防重复删除机制 - 跟踪正在删除的文件
|
||||
this.deletingFiles = new Set();
|
||||
|
||||
// 默认缓存配置
|
||||
this.config = {
|
||||
maxAge: 5 * 60 * 1000, // 5分钟缓存(API数据变化较快)
|
||||
@@ -298,6 +301,12 @@ class ApiCacheService {
|
||||
// 计算总大小和收集文件信息
|
||||
for (const file of files) {
|
||||
const filePath = path.join(this.cacheDir, file);
|
||||
|
||||
// 防止重复删除同一个文件
|
||||
if (this.deletingFiles.has(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
totalSize += stats.size;
|
||||
@@ -323,21 +332,43 @@ class ApiCacheService {
|
||||
// 按修改时间排序,删除最旧的文件
|
||||
fileStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
|
||||
|
||||
let deletedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const file of fileStats) {
|
||||
const deleteSuccess = await FileUtils.safeDeleteFile(file.path);
|
||||
if (deleteSuccess) {
|
||||
totalSize -= file.size;
|
||||
logger.debug(`删除过大API缓存文件: ${file.path}`);
|
||||
} else {
|
||||
logger.debug(`删除过大API缓存文件失败: ${file.path}`);
|
||||
// 防止重复删除
|
||||
if (this.deletingFiles.has(file.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (totalSize <= this.config.maxSize * 0.8) { // 清理到80%
|
||||
break;
|
||||
// 标记文件为删除中
|
||||
this.deletingFiles.add(file.path);
|
||||
|
||||
try {
|
||||
const deleteSuccess = await this.safeDeleteFileWithRetry(file.path);
|
||||
if (deleteSuccess) {
|
||||
totalSize -= file.size;
|
||||
deletedCount++;
|
||||
logger.debug(`删除过大API缓存文件: ${file.path}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
logger.debug(`删除过大API缓存文件失败: ${file.path}`);
|
||||
}
|
||||
|
||||
if (totalSize <= this.config.maxSize * 0.8) { // 清理到80%
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
// 移除删除标记
|
||||
this.deletingFiles.delete(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`API缓存清理完成,当前大小: ${totalSize}`);
|
||||
if (errorCount > 0) {
|
||||
logger.info(`API缓存清理完成,当前大小: ${totalSize},成功删除 ${deletedCount} 个文件,失败 ${errorCount} 个文件`);
|
||||
} else {
|
||||
logger.info(`API缓存清理完成,当前大小: ${totalSize},删除了 ${deletedCount} 个文件`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('检查API缓存大小失败:', error);
|
||||
@@ -355,38 +386,45 @@ class ApiCacheService {
|
||||
let errorCount = 0;
|
||||
let skippedCount = 0;
|
||||
const now = Date.now();
|
||||
const errorDetails = {
|
||||
permission: 0,
|
||||
busy: 0,
|
||||
system: 0,
|
||||
other: 0
|
||||
};
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(this.cacheDir, file);
|
||||
|
||||
// 防止重复删除同一个文件
|
||||
if (this.deletingFiles.has(filePath)) {
|
||||
logger.debug(`API缓存文件正在删除中,跳过: ${filePath}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
const age = now - stats.mtime.getTime();
|
||||
if (age > this.config.maxAge) {
|
||||
// 简单的文件占用检查(仅在Windows上)
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
// 尝试以独占模式打开文件来检查是否被占用
|
||||
const handle = await fs.open(filePath, 'r');
|
||||
await handle.close();
|
||||
} catch (openError) {
|
||||
if (openError.code === 'EBUSY' || openError.code === 'EPERM') {
|
||||
logger.debug(`跳过被占用的过期API缓存文件: ${filePath}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 标记文件为删除中
|
||||
this.deletingFiles.add(filePath);
|
||||
|
||||
// 使用改进的安全删除方法
|
||||
const deleteSuccess = await FileUtils.safeDeleteFile(filePath);
|
||||
if (deleteSuccess) {
|
||||
cleanedCount++;
|
||||
logger.debug(`成功删除过期API缓存文件: ${filePath}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
// 减少日志噪音,只在debug级别记录
|
||||
logger.debug(`删除过期API缓存文件失败: ${filePath}`);
|
||||
try {
|
||||
// 使用带重试的删除方法
|
||||
const deleteSuccess = await this.safeDeleteFileWithRetry(filePath);
|
||||
if (deleteSuccess) {
|
||||
cleanedCount++;
|
||||
logger.debug(`成功删除过期API缓存文件: ${filePath}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
errorDetails.other++;
|
||||
logger.debug(`删除过期API缓存文件失败: ${filePath}`);
|
||||
}
|
||||
} finally {
|
||||
// 无论成功失败,都要移除删除标记
|
||||
this.deletingFiles.delete(filePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -394,9 +432,28 @@ class ApiCacheService {
|
||||
if (error.code === 'ENOENT') {
|
||||
logger.debug(`过期API缓存文件不存在,跳过: ${filePath}`);
|
||||
} else {
|
||||
const errorInfo = this.categorizeError(error);
|
||||
logger.debug(`检查过期API缓存文件失败: ${filePath}`, error.message);
|
||||
errorCount++;
|
||||
|
||||
// 统计错误类型
|
||||
switch (errorInfo.type) {
|
||||
case 'permission':
|
||||
errorDetails.permission++;
|
||||
break;
|
||||
case 'busy':
|
||||
errorDetails.busy++;
|
||||
break;
|
||||
case 'system':
|
||||
errorDetails.system++;
|
||||
break;
|
||||
default:
|
||||
errorDetails.other++;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除删除标记
|
||||
this.deletingFiles.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +461,21 @@ class ApiCacheService {
|
||||
if (errorCount === 0 && skippedCount === 0) {
|
||||
logger.info(`清理了 ${cleanedCount} 个过期API缓存文件`);
|
||||
} else {
|
||||
logger.info(`API缓存清理完成,成功删除 ${cleanedCount} 个文件,失败 ${errorCount} 个文件,跳过 ${skippedCount} 个被占用文件`);
|
||||
let errorMsg = `API缓存清理完成,成功删除 ${cleanedCount} 个文件,失败 ${errorCount} 个文件,跳过 ${skippedCount} 个被占用文件`;
|
||||
|
||||
if (errorCount > 0) {
|
||||
const errorBreakdown = [];
|
||||
if (errorDetails.permission > 0) errorBreakdown.push(`权限错误 ${errorDetails.permission} 个`);
|
||||
if (errorDetails.busy > 0) errorBreakdown.push(`文件占用 ${errorDetails.busy} 个`);
|
||||
if (errorDetails.system > 0) errorBreakdown.push(`系统错误 ${errorDetails.system} 个`);
|
||||
if (errorDetails.other > 0) errorBreakdown.push(`其他错误 ${errorDetails.other} 个`);
|
||||
|
||||
if (errorBreakdown.length > 0) {
|
||||
errorMsg += ` (${errorBreakdown.join(', ')})`;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(errorMsg);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -430,39 +501,94 @@ class ApiCacheService {
|
||||
async clearAllCache() {
|
||||
try {
|
||||
const files = await fs.readdir(this.cacheDir);
|
||||
let cleanedCount = 0;
|
||||
let errorCount = 0;
|
||||
let deletedCount = 0;
|
||||
let failedCount = 0;
|
||||
let skippedCount = 0;
|
||||
const errorDetails = {
|
||||
permission: 0,
|
||||
busy: 0,
|
||||
system: 0,
|
||||
other: 0
|
||||
};
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(this.cacheDir, file);
|
||||
|
||||
// 防止重复删除同一个文件
|
||||
if (this.deletingFiles.has(filePath)) {
|
||||
logger.debug(`API缓存文件正在删除中,跳过: ${filePath}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 标记文件为删除中
|
||||
this.deletingFiles.add(filePath);
|
||||
|
||||
try {
|
||||
const deleteSuccess = await FileUtils.safeDeleteFile(filePath);
|
||||
// 使用带重试的删除方法
|
||||
const deleteSuccess = await this.safeDeleteFileWithRetry(filePath);
|
||||
if (deleteSuccess) {
|
||||
cleanedCount++;
|
||||
logger.debug(`删除API缓存文件: ${filePath}`);
|
||||
deletedCount++;
|
||||
logger.debug(`成功删除API缓存文件: ${filePath}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
failedCount++;
|
||||
errorDetails.other++;
|
||||
logger.debug(`删除API缓存文件失败: ${filePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果文件不存在,静默忽略
|
||||
if (error.code === 'ENOENT') {
|
||||
logger.debug(`API缓存文件不存在,跳过: ${filePath}`);
|
||||
} else {
|
||||
logger.debug(`删除API缓存文件出错: ${filePath}`, error.message);
|
||||
errorCount++;
|
||||
const errorInfo = this.categorizeError(error);
|
||||
logger.debug(`删除API缓存文件时发生错误: ${filePath}`, error.message);
|
||||
failedCount++;
|
||||
|
||||
// 统计错误类型
|
||||
switch (errorInfo.type) {
|
||||
case 'permission':
|
||||
errorDetails.permission++;
|
||||
break;
|
||||
case 'busy':
|
||||
errorDetails.busy++;
|
||||
break;
|
||||
case 'system':
|
||||
errorDetails.system++;
|
||||
break;
|
||||
default:
|
||||
errorDetails.other++;
|
||||
}
|
||||
} finally {
|
||||
// 无论成功失败,都要移除删除标记
|
||||
this.deletingFiles.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount === 0) {
|
||||
logger.info(`所有API缓存已清理,共删除 ${cleanedCount} 个文件`);
|
||||
// 清空缓存索引
|
||||
this.cacheIndex.clear();
|
||||
await this.saveCacheIndex();
|
||||
|
||||
if (deletedCount > 0 || failedCount > 0 || skippedCount > 0) {
|
||||
if (failedCount === 0 && skippedCount === 0) {
|
||||
logger.info(`成功清空所有API缓存,删除了 ${deletedCount} 个文件`);
|
||||
} else {
|
||||
let errorMsg = `API缓存清空完成,成功删除 ${deletedCount} 个文件,失败 ${failedCount} 个文件,跳过 ${skippedCount} 个被占用文件`;
|
||||
|
||||
if (failedCount > 0) {
|
||||
const errorBreakdown = [];
|
||||
if (errorDetails.permission > 0) errorBreakdown.push(`权限错误 ${errorDetails.permission} 个`);
|
||||
if (errorDetails.busy > 0) errorBreakdown.push(`文件占用 ${errorDetails.busy} 个`);
|
||||
if (errorDetails.system > 0) errorBreakdown.push(`系统错误 ${errorDetails.system} 个`);
|
||||
if (errorDetails.other > 0) errorBreakdown.push(`其他错误 ${errorDetails.other} 个`);
|
||||
|
||||
if (errorBreakdown.length > 0) {
|
||||
errorMsg += ` (${errorBreakdown.join(', ')})`;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(errorMsg);
|
||||
}
|
||||
} else {
|
||||
logger.info(`API缓存清理完成,成功删除 ${cleanedCount} 个文件,失败 ${errorCount} 个文件`);
|
||||
logger.info('API缓存目录为空,无需清理');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('清理所有API缓存失败:', error);
|
||||
throw error;
|
||||
logger.error('清空API缓存失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +669,86 @@ class ApiCacheService {
|
||||
this.config = { ...this.config, ...defaultConfig };
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试的安全删除文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {number} maxRetries 最大重试次数
|
||||
* @returns {Promise<boolean>} 删除是否成功
|
||||
*/
|
||||
async safeDeleteFileWithRetry(filePath, maxRetries = 3) {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const success = await FileUtils.safeDeleteFile(filePath);
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果删除失败但不是最后一次尝试,等待后重试
|
||||
if (attempt < maxRetries) {
|
||||
const delay = Math.min(1000 * attempt, 5000); // 递增延迟,最大5秒
|
||||
logger.debug(`删除API缓存文件失败,${delay}ms后重试 (${attempt}/${maxRetries}): ${filePath}`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`删除API缓存文件异常 (${attempt}/${maxRetries}): ${filePath}`, error.message);
|
||||
|
||||
// 如果是最后一次尝试,返回失败
|
||||
if (attempt === maxRetries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 等待后重试
|
||||
const delay = Math.min(1000 * attempt, 5000);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类错误类型
|
||||
* @param {Error} error 错误对象
|
||||
* @returns {Object} 错误分类信息
|
||||
*/
|
||||
categorizeError(error) {
|
||||
const errorInfo = {
|
||||
type: 'unknown',
|
||||
retryable: false,
|
||||
message: error.message || '未知错误'
|
||||
};
|
||||
|
||||
if (!error.code) {
|
||||
return errorInfo;
|
||||
}
|
||||
|
||||
switch (error.code) {
|
||||
case 'EPERM':
|
||||
case 'EACCES':
|
||||
errorInfo.type = 'permission';
|
||||
errorInfo.retryable = true;
|
||||
break;
|
||||
case 'EBUSY':
|
||||
errorInfo.type = 'busy';
|
||||
errorInfo.retryable = true;
|
||||
break;
|
||||
case 'ENOENT':
|
||||
errorInfo.type = 'not_found';
|
||||
errorInfo.retryable = false; // 文件不存在,不需要重试
|
||||
break;
|
||||
case 'EMFILE':
|
||||
case 'ENFILE':
|
||||
errorInfo.type = 'resource';
|
||||
errorInfo.retryable = true;
|
||||
break;
|
||||
default:
|
||||
errorInfo.type = 'system';
|
||||
errorInfo.retryable = false;
|
||||
}
|
||||
|
||||
return errorInfo;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ApiCacheService;
|
||||
@@ -324,7 +324,7 @@ class DownloadRegistry {
|
||||
totalArtworks: this.getTotalArtworkCount()
|
||||
};
|
||||
|
||||
logger.info('注册表导入完成', result);
|
||||
logger.info(`注册表导入完成,导入了 ${result.addedArtists} 个作者,${result.addedArtworks} 个作品,跳过了 ${result.skippedArtworks} 个重复作品。当前总计:${result.totalArtists} 个作者,${result.totalArtworks} 个作品`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+274
-73
@@ -53,6 +53,9 @@ class ImageCacheService {
|
||||
// 缓存索引
|
||||
this.cacheIndex = new Map();
|
||||
|
||||
// 添加删除状态跟踪,防止循环删除
|
||||
this.deletingFiles = new Set();
|
||||
|
||||
// 初始化配置
|
||||
this.initializeConfig();
|
||||
}
|
||||
@@ -287,6 +290,12 @@ class ImageCacheService {
|
||||
// 使用索引计算总大小和收集文件信息
|
||||
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
|
||||
const filePath = path.join(this.cacheDir, fileInfo.filename);
|
||||
|
||||
// 防止重复删除同一个文件
|
||||
if (this.deletingFiles.has(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证文件是否实际存在
|
||||
const stats = await fs.stat(filePath);
|
||||
@@ -315,39 +324,149 @@ class ImageCacheService {
|
||||
// 按修改时间排序,删除最旧的文件
|
||||
fileStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
|
||||
|
||||
for (const file of fileStats) {
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
totalSize -= file.size;
|
||||
let deletedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// 从索引中移除
|
||||
this.removeFromIndex(file.cacheKey);
|
||||
for (const file of fileStats) {
|
||||
// 防止重复删除
|
||||
if (this.deletingFiles.has(file.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 标记文件为删除中
|
||||
this.deletingFiles.add(file.path);
|
||||
|
||||
try {
|
||||
const deleteSuccess = await this.safeDeleteFileWithRetry(file.path);
|
||||
if (deleteSuccess) {
|
||||
totalSize -= file.size;
|
||||
deletedCount++;
|
||||
// 从索引中移除
|
||||
this.removeFromIndex(file.cacheKey);
|
||||
logger.debug(`成功删除缓存文件: ${file.path}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
logger.debug(`删除缓存文件失败: ${file.path}`);
|
||||
// 即使删除失败,也从索引中移除,避免重复尝试
|
||||
this.removeFromIndex(file.cacheKey);
|
||||
}
|
||||
|
||||
if (totalSize <= this.config.maxSize * 0.8) { // 清理到80%
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果删除文件失败,记录日志但继续处理其他文件
|
||||
if (error.code === 'ENOENT') {
|
||||
logger.warn(`删除缓存文件时文件不存在: ${file.path}`);
|
||||
// 从索引中移除
|
||||
this.removeFromIndex(file.cacheKey);
|
||||
} else {
|
||||
logger.error(`删除缓存文件失败: ${file.path}`, error);
|
||||
}
|
||||
} finally {
|
||||
// 移除删除标记
|
||||
this.deletingFiles.delete(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存更新后的索引
|
||||
await this.saveCacheIndex();
|
||||
|
||||
logger.info(`缓存清理完成,当前大小: ${totalSize}`);
|
||||
if (errorCount > 0) {
|
||||
logger.info(`缓存清理完成,当前大小: ${totalSize},成功删除 ${deletedCount} 个文件,失败 ${errorCount} 个文件`);
|
||||
} else {
|
||||
logger.info(`缓存清理完成,当前大小: ${totalSize},删除了 ${deletedCount} 个文件`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('检查缓存大小失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动清理所有缓存
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clearAllCache() {
|
||||
try {
|
||||
let deletedCount = 0;
|
||||
let errorCount = 0;
|
||||
const errorDetails = {
|
||||
permission: 0,
|
||||
busy: 0,
|
||||
system: 0,
|
||||
other: 0
|
||||
};
|
||||
|
||||
// 使用索引清理所有文件
|
||||
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
|
||||
const filePath = path.join(this.cacheDir, fileInfo.filename);
|
||||
|
||||
// 防止重复删除
|
||||
if (this.deletingFiles.has(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 标记文件为删除中
|
||||
this.deletingFiles.add(filePath);
|
||||
|
||||
try {
|
||||
// 使用带重试的删除方法
|
||||
const deleteSuccess = await this.safeDeleteFileWithRetry(filePath);
|
||||
if (deleteSuccess) {
|
||||
deletedCount++;
|
||||
logger.debug(`成功删除缓存文件: ${filePath}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
errorDetails.other++;
|
||||
logger.debug(`删除缓存文件失败: ${filePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorInfo = this.categorizeError(error);
|
||||
errorCount++;
|
||||
|
||||
// 统计错误类型
|
||||
switch (errorInfo.type) {
|
||||
case 'permission':
|
||||
errorDetails.permission++;
|
||||
break;
|
||||
case 'busy':
|
||||
errorDetails.busy++;
|
||||
break;
|
||||
case 'system':
|
||||
errorDetails.system++;
|
||||
break;
|
||||
default:
|
||||
errorDetails.other++;
|
||||
}
|
||||
|
||||
logger.debug(`删除缓存文件异常: ${filePath}`, error.message);
|
||||
} finally {
|
||||
// 移除删除标记
|
||||
this.deletingFiles.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空索引
|
||||
this.cacheIndex.clear();
|
||||
// 清空删除标记
|
||||
this.deletingFiles.clear();
|
||||
await this.saveCacheIndex();
|
||||
|
||||
if (errorCount === 0) {
|
||||
logger.info(`所有缓存已清理,共删除 ${deletedCount} 个文件`);
|
||||
} else {
|
||||
let errorMsg = `缓存清理完成,成功删除 ${deletedCount} 个文件,失败 ${errorCount} 个文件`;
|
||||
|
||||
const errorBreakdown = [];
|
||||
if (errorDetails.permission > 0) errorBreakdown.push(`权限错误 ${errorDetails.permission} 个`);
|
||||
if (errorDetails.busy > 0) errorBreakdown.push(`文件占用 ${errorDetails.busy} 个`);
|
||||
if (errorDetails.system > 0) errorBreakdown.push(`系统错误 ${errorDetails.system} 个`);
|
||||
if (errorDetails.other > 0) errorBreakdown.push(`其他错误 ${errorDetails.other} 个`);
|
||||
|
||||
if (errorBreakdown.length > 0) {
|
||||
errorMsg += ` (${errorBreakdown.join(', ')})`;
|
||||
}
|
||||
|
||||
logger.info(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('清理所有缓存失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
* @returns {Promise<void>}
|
||||
@@ -358,42 +477,49 @@ class ImageCacheService {
|
||||
let errorCount = 0;
|
||||
let skippedCount = 0;
|
||||
const now = Date.now();
|
||||
const errorDetails = {
|
||||
permission: 0,
|
||||
busy: 0,
|
||||
system: 0,
|
||||
other: 0
|
||||
};
|
||||
|
||||
// 使用索引检查过期文件
|
||||
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
|
||||
const filePath = path.join(this.cacheDir, fileInfo.filename);
|
||||
|
||||
// 防止重复删除同一个文件
|
||||
if (this.deletingFiles.has(filePath)) {
|
||||
logger.debug(`文件正在删除中,跳过: ${filePath}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
const age = now - stats.mtime.getTime();
|
||||
if (age > this.config.maxAge) {
|
||||
// 简单的文件占用检查(仅在Windows上)
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
// 尝试以独占模式打开文件来检查是否被占用
|
||||
const handle = await fs.open(filePath, 'r');
|
||||
await handle.close();
|
||||
} catch (openError) {
|
||||
if (openError.code === 'EBUSY' || openError.code === 'EPERM') {
|
||||
logger.debug(`跳过被占用的过期缓存文件: ${filePath}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 标记文件为删除中
|
||||
this.deletingFiles.add(filePath);
|
||||
|
||||
// 使用改进的安全删除方法
|
||||
const deleteSuccess = await FileUtils.safeDeleteFile(filePath);
|
||||
if (deleteSuccess) {
|
||||
this.removeFromIndex(cacheKey);
|
||||
cleanedCount++;
|
||||
logger.debug(`成功删除过期缓存文件: ${filePath}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
// 减少日志噪音,只在debug级别记录
|
||||
logger.debug(`删除过期缓存文件失败: ${filePath}`);
|
||||
// 即使删除失败,也从索引中移除,避免重复尝试
|
||||
this.removeFromIndex(cacheKey);
|
||||
try {
|
||||
// 使用带重试的删除方法
|
||||
const deleteSuccess = await this.safeDeleteFileWithRetry(filePath);
|
||||
if (deleteSuccess) {
|
||||
this.removeFromIndex(cacheKey);
|
||||
cleanedCount++;
|
||||
logger.debug(`成功删除过期缓存文件: ${filePath}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
errorDetails.other++;
|
||||
logger.debug(`删除过期缓存文件失败: ${filePath}`);
|
||||
// 即使删除失败,也从索引中移除,避免重复尝试
|
||||
this.removeFromIndex(cacheKey);
|
||||
}
|
||||
} finally {
|
||||
// 无论成功失败,都要移除删除标记
|
||||
this.deletingFiles.delete(filePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -402,19 +528,53 @@ class ImageCacheService {
|
||||
logger.debug(`过期缓存文件不存在,从索引中移除: ${filePath}`);
|
||||
this.removeFromIndex(cacheKey);
|
||||
} else {
|
||||
const errorInfo = this.categorizeError(error);
|
||||
logger.debug(`检查过期缓存文件失败: ${filePath}`, error.message);
|
||||
errorCount++;
|
||||
|
||||
// 统计错误类型
|
||||
switch (errorInfo.type) {
|
||||
case 'permission':
|
||||
errorDetails.permission++;
|
||||
break;
|
||||
case 'busy':
|
||||
errorDetails.busy++;
|
||||
break;
|
||||
case 'system':
|
||||
errorDetails.system++;
|
||||
break;
|
||||
default:
|
||||
errorDetails.other++;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除删除标记
|
||||
this.deletingFiles.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0 || errorCount > 0 || skippedCount > 0) {
|
||||
// 保存更新后的索引
|
||||
await this.saveCacheIndex();
|
||||
|
||||
if (errorCount === 0 && skippedCount === 0) {
|
||||
logger.info(`清理了 ${cleanedCount} 个过期缓存文件`);
|
||||
} else {
|
||||
logger.info(`缓存清理完成,成功删除 ${cleanedCount} 个文件,失败 ${errorCount} 个文件,跳过 ${skippedCount} 个被占用文件`);
|
||||
let errorMsg = `缓存清理完成,成功删除 ${cleanedCount} 个文件,失败 ${errorCount} 个文件,跳过 ${skippedCount} 个被占用文件`;
|
||||
|
||||
if (errorCount > 0) {
|
||||
const errorBreakdown = [];
|
||||
if (errorDetails.permission > 0) errorBreakdown.push(`权限错误 ${errorDetails.permission} 个`);
|
||||
if (errorDetails.busy > 0) errorBreakdown.push(`文件占用 ${errorDetails.busy} 个`);
|
||||
if (errorDetails.system > 0) errorBreakdown.push(`系统错误 ${errorDetails.system} 个`);
|
||||
if (errorDetails.other > 0) errorBreakdown.push(`其他错误 ${errorDetails.other} 个`);
|
||||
|
||||
if (errorBreakdown.length > 0) {
|
||||
errorMsg += ` (${errorBreakdown.join(', ')})`;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(errorMsg);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -458,42 +618,83 @@ class ImageCacheService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动清理所有缓存
|
||||
* @returns {Promise<void>}
|
||||
* 带重试的安全删除文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {number} maxRetries 最大重试次数
|
||||
* @returns {Promise<boolean>} 删除是否成功
|
||||
*/
|
||||
async clearAllCache() {
|
||||
try {
|
||||
let deletedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// 使用索引清理所有文件
|
||||
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
|
||||
const filePath = path.join(this.cacheDir, fileInfo.filename);
|
||||
|
||||
// 使用改进的安全删除方法
|
||||
const deleteSuccess = await FileUtils.safeDeleteFile(filePath);
|
||||
if (deleteSuccess) {
|
||||
deletedCount++;
|
||||
logger.debug(`成功删除缓存文件: ${filePath}`);
|
||||
} else {
|
||||
errorCount++;
|
||||
logger.debug(`删除缓存文件失败: ${filePath}`);
|
||||
async safeDeleteFileWithRetry(filePath, maxRetries = 3) {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const success = await FileUtils.safeDeleteFile(filePath);
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 清空索引
|
||||
this.cacheIndex.clear();
|
||||
await this.saveCacheIndex();
|
||||
// 如果删除失败但不是最后一次尝试,等待后重试
|
||||
if (attempt < maxRetries) {
|
||||
const delay = Math.min(1000 * attempt, 5000); // 递增延迟,最大5秒
|
||||
logger.debug(`删除文件失败,${delay}ms后重试 (${attempt}/${maxRetries}): ${filePath}`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`删除文件异常 (${attempt}/${maxRetries}): ${filePath}`, error.message);
|
||||
|
||||
if (errorCount === 0) {
|
||||
logger.info(`所有缓存已清理,共删除 ${deletedCount} 个文件`);
|
||||
} else {
|
||||
logger.info(`缓存清理完成,成功删除 ${deletedCount} 个文件,失败 ${errorCount} 个文件`);
|
||||
// 如果是最后一次尝试,返回失败
|
||||
if (attempt === maxRetries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 等待后重试
|
||||
const delay = Math.min(1000 * attempt, 5000);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('清理所有缓存失败:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类错误类型
|
||||
* @param {Error} error 错误对象
|
||||
* @returns {Object} 错误分类信息
|
||||
*/
|
||||
categorizeError(error) {
|
||||
const errorInfo = {
|
||||
type: 'unknown',
|
||||
retryable: false,
|
||||
message: error.message || '未知错误'
|
||||
};
|
||||
|
||||
if (!error.code) {
|
||||
return errorInfo;
|
||||
}
|
||||
|
||||
switch (error.code) {
|
||||
case 'EPERM':
|
||||
case 'EACCES':
|
||||
errorInfo.type = 'permission';
|
||||
errorInfo.retryable = true;
|
||||
break;
|
||||
case 'EBUSY':
|
||||
errorInfo.type = 'busy';
|
||||
errorInfo.retryable = true;
|
||||
break;
|
||||
case 'ENOENT':
|
||||
errorInfo.type = 'not_found';
|
||||
errorInfo.retryable = false; // 文件不存在,不需要重试
|
||||
break;
|
||||
case 'EMFILE':
|
||||
case 'ENFILE':
|
||||
errorInfo.type = 'resource';
|
||||
errorInfo.retryable = true;
|
||||
break;
|
||||
default:
|
||||
errorInfo.type = 'system';
|
||||
errorInfo.retryable = false;
|
||||
}
|
||||
|
||||
return errorInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+68
-19
@@ -21,6 +21,15 @@ class FileUtils {
|
||||
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}`);
|
||||
@@ -29,25 +38,7 @@ class FileUtils {
|
||||
// 如果是权限错误,尝试Windows特定的删除方法
|
||||
if (error.code === 'EPERM' || error.code === 'EACCES') {
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
// 尝试修改文件属性后删除
|
||||
const nativeFs = require('fs').promises;
|
||||
|
||||
try {
|
||||
await nativeFs.chmod(filePath, 0o666);
|
||||
} catch (chmodError) {
|
||||
// 忽略chmod错误
|
||||
logger.debug(`修改文件权限失败: ${filePath}`, chmodError.message);
|
||||
}
|
||||
|
||||
// 再次尝试删除
|
||||
await nativeFs.unlink(filePath);
|
||||
logger.info(`修改权限后删除成功: ${filePath}`);
|
||||
return true;
|
||||
} catch (forceError) {
|
||||
logger.warn(`Windows权限删除失败: ${filePath}`, forceError.message);
|
||||
return false;
|
||||
}
|
||||
return await this.forceDeleteFileWindows(filePath);
|
||||
} else {
|
||||
logger.warn(`删除文件权限不足: ${filePath}`, error.message);
|
||||
return false;
|
||||
@@ -60,11 +51,69 @@ class FileUtils {
|
||||
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 打包)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user