From 194566c8fba12d78fb7350651b31ee365372eb9b Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Wed, 1 Oct 2025 19:26:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=93=E5=AD=98=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/services/api-cache.js | 312 +++++++++++++++++++---- backend/services/download-registry.js | 2 +- backend/services/image-cache.js | 351 ++++++++++++++++++++------ backend/utils/file-utils.js | 87 +++++-- 4 files changed, 604 insertions(+), 148 deletions(-) diff --git a/backend/services/api-cache.js b/backend/services/api-cache.js index 5302b42..48f5d6e 100644 --- a/backend/services/api-cache.js +++ b/backend/services/api-cache.js @@ -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} 删除是否成功 + */ + 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; \ No newline at end of file +module.exports = ApiCacheService; \ No newline at end of file diff --git a/backend/services/download-registry.js b/backend/services/download-registry.js index 7dc27d0..96691ca 100644 --- a/backend/services/download-registry.js +++ b/backend/services/download-registry.js @@ -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; } diff --git a/backend/services/image-cache.js b/backend/services/image-cache.js index 08ecb96..d5f671e 100644 --- a/backend/services/image-cache.js +++ b/backend/services/image-cache.js @@ -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()); + let deletedCount = 0; + let errorCount = 0; + for (const file of fileStats) { + // 防止重复删除 + if (this.deletingFiles.has(file.path)) { + continue; + } + + // 标记文件为删除中 + this.deletingFiles.add(file.path); + try { - await fs.unlink(file.path); - totalSize -= file.size; - - // 从索引中移除 - this.removeFromIndex(file.cacheKey); + 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} + */ + 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} @@ -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} + * 带重试的安全删除文件 + * @param {string} filePath 文件路径 + * @param {number} maxRetries 最大重试次数 + * @returns {Promise} 删除是否成功 */ - 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; } + + // 如果删除失败但不是最后一次尝试,等待后重试 + 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 (attempt === maxRetries) { + return false; + } + + // 等待后重试 + const delay = Math.min(1000 * attempt, 5000); + await new Promise(resolve => setTimeout(resolve, delay)); } - - // 清空索引 - this.cacheIndex.clear(); - await this.saveCacheIndex(); - - if (errorCount === 0) { - logger.info(`所有缓存已清理,共删除 ${deletedCount} 个文件`); - } else { - logger.info(`缓存清理完成,成功删除 ${deletedCount} 个文件,失败 ${errorCount} 个文件`); - } - } 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; } /** @@ -703,4 +904,4 @@ class ImageCacheService { } } -module.exports = ImageCacheService; \ No newline at end of file +module.exports = ImageCacheService; \ No newline at end of file diff --git a/backend/utils/file-utils.js b/backend/utils/file-utils.js index 5422d9e..d3bf754 100644 --- a/backend/utils/file-utils.js +++ b/backend/utils/file-utils.js @@ -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 打包) */