diff --git a/backend/config/cache-config.js b/backend/config/cache-config.js index 89c955d..f51fe92 100644 --- a/backend/config/cache-config.js +++ b/backend/config/cache-config.js @@ -26,29 +26,27 @@ class CacheConfigManager { // 确保路径是绝对路径 this.configPath = path.resolve(this.configPath); - // 默认配置 - this.defaultConfig = { - // 缓存配置 + // 默认缓存配置 + this.config = { maxAge: 24 * 60 * 60 * 1000, // 24小时缓存 maxSize: 100 * 1024 * 1024, // 100MB最大缓存大小 - cleanupInterval: 60 * 60 * 1000, // 1小时清理一次 - - // 缓存启用状态 + cleanupInterval: 2 * 60 * 60 * 1000, // 2小时清理一次(原来是1小时) enabled: true, - - // 代理配置 proxy: { enabled: true, - timeout: 30000, // 30秒超时 - retryCount: 3, // 重试次数 - retryDelay: 1000, // 重试延迟(毫秒) + timeout: 30000, + retryCount: 3, + retryDelay: 1000, }, - - // 文件类型过滤 allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'], - - // 最后更新时间 - lastUpdated: new Date().toISOString() + // 新增Windows特定配置 + windows: { + skipInUseFiles: true, // 跳过被占用的文件 + maxRetries: 3, // 最大重试次数 + retryDelay: 2000, // 重试延迟(毫秒) + waitForRelease: true, // 等待文件释放 + maxWaitTime: 10000, // 最大等待时间(毫秒) + } }; // 确保配置目录存在 @@ -90,7 +88,7 @@ class CacheConfigManager { */ async createDefaultConfig() { try { - const configContent = JSON.stringify(this.defaultConfig, null, 2); + const configContent = JSON.stringify(this.config, null, 2); await fs.writeFile(this.configPath, configContent, 'utf8'); // logger.info('默认缓存配置文件创建成功:', this.configPath); } catch (error) { @@ -108,10 +106,10 @@ class CacheConfigManager { const config = JSON.parse(configContent); // 合并默认配置,确保所有字段都存在 - return { ...this.defaultConfig, ...config }; + return { ...this.config, ...config }; } catch (error) { logger.error('加载缓存配置失败:', error); - return this.defaultConfig; + return this.config; } } @@ -152,9 +150,9 @@ class CacheConfigManager { */ async resetToDefault() { try { - await this.saveConfig(this.defaultConfig); + await this.saveConfig(this.config); logger.info('缓存配置已重置为默认值'); - return this.defaultConfig; + return this.config; } catch (error) { logger.error('重置缓存配置失败:', error); throw error; diff --git a/backend/services/file-manager.js b/backend/services/file-manager.js index a30b82f..e066235 100644 --- a/backend/services/file-manager.js +++ b/backend/services/file-manager.js @@ -107,6 +107,9 @@ class FileManager { let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { + let writer = null; + let response = null; + try { // 使用增强的文件工具类确保目录存在 const dirPath = path.dirname(filePath); @@ -124,7 +127,7 @@ class FileManager { } } - const response = await axios({ + response = await axios({ method: 'GET', url: url, responseType: 'stream', @@ -136,30 +139,76 @@ class FileManager { }); // 使用增强的写入流创建方法 - const writer = await FileUtils.safeCreateWriteStream(filePath); + writer = await FileUtils.safeCreateWriteStream(filePath); response.data.pipe(writer); return new Promise((resolve, reject) => { - writer.on('finish', () => { - // 验证文件是否写入成功 - fs.access(filePath) - .then(() => resolve()) - .catch(error => { - logger.error(`文件写入验证失败: ${filePath}`, error.message); - reject(error); - }); + let isResolved = false; + + const cleanup = () => { + if (writer && !writer.destroyed) { + writer.destroy(); + } + if (response && response.data && !response.data.destroyed) { + response.data.destroy(); + } + }; + + writer.on('finish', async () => { + if (isResolved) return; + isResolved = true; + + try { + // 验证文件是否写入成功 + await fs.access(filePath); + cleanup(); + resolve(); + } catch (error) { + logger.error(`文件写入验证失败: ${filePath}`, error.message); + cleanup(); + reject(error); + } }); + writer.on('error', async (error) => { + if (isResolved) return; + isResolved = true; + // 下载失败时删除文件 try { await this.safeDeleteFile(filePath); } catch (removeError) { logger.warn('清理失败文件时出错:', removeError.message); } + + cleanup(); reject(error); }); + + // 添加超时处理 + const timeout = setTimeout(() => { + if (isResolved) return; + isResolved = true; + + const timeoutError = new Error('下载超时'); + cleanup(); + reject(timeoutError); + }, 120000); // 2分钟超时 + + // 清理超时定时器 + writer.on('finish', () => clearTimeout(timeout)); + writer.on('error', () => clearTimeout(timeout)); }); + } catch (error) { + // 确保清理资源 + if (writer && !writer.destroyed) { + writer.destroy(); + } + if (response && response.data && !response.data.destroyed) { + response.data.destroy(); + } + lastError = error; // 处理文件系统错误 diff --git a/backend/services/image-cache.js b/backend/services/image-cache.js index 988631f..6a88646 100644 --- a/backend/services/image-cache.js +++ b/backend/services/image-cache.js @@ -4,6 +4,7 @@ const crypto = require('crypto'); const axios = require('axios'); const CacheConfigManager = require('../config/cache-config'); const { defaultLogger } = require('../utils/logger'); +const FileUtils = require('../utils/file-utils'); // 创建logger实例 const logger = defaultLogger.child('ImageCache'); @@ -354,6 +355,8 @@ class ImageCacheService { async cleanupExpiredCache() { try { let cleanedCount = 0; + let errorCount = 0; + let skippedCount = 0; const now = Date.now(); // 使用索引检查过期文件 @@ -364,46 +367,91 @@ class ImageCacheService { const age = now - stats.mtime.getTime(); if (age > this.config.maxAge) { - try { - await fs.unlink(filePath); + // 简单的文件占用检查(仅在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; + } + } + } + + // 使用改进的安全删除方法 + const deleteSuccess = await FileUtils.safeDeleteFile(filePath); + if (deleteSuccess) { this.removeFromIndex(cacheKey); cleanedCount++; - } catch (deleteError) { - if (deleteError.code === 'ENOENT') { - logger.warn(`删除过期缓存文件时文件不存在: ${filePath}`); - this.removeFromIndex(cacheKey); - } else { - logger.error(`删除过期缓存文件失败: ${filePath}`, deleteError); - } + logger.debug(`成功删除过期缓存文件: ${filePath}`); + } else { + errorCount++; + // 减少日志噪音,只在debug级别记录 + logger.debug(`删除过期缓存文件失败: ${filePath}`); + // 即使删除失败,也从索引中移除,避免重复尝试 + this.removeFromIndex(cacheKey); } } } catch (error) { // 如果文件不存在,从索引中移除 if (error.code === 'ENOENT') { - logger.warn(`过期缓存文件不存在,从索引中移除: ${filePath}`); + logger.debug(`过期缓存文件不存在,从索引中移除: ${filePath}`); this.removeFromIndex(cacheKey); } else { - logger.error(`检查过期缓存文件失败: ${filePath}`, error); + logger.debug(`检查过期缓存文件失败: ${filePath}`, error.message); + errorCount++; } } } - if (cleanedCount > 0) { + if (cleanedCount > 0 || errorCount > 0 || skippedCount > 0) { // 保存更新后的索引 await this.saveCacheIndex(); - logger.info(`清理了 ${cleanedCount} 个过期缓存文件`); + if (errorCount === 0 && skippedCount === 0) { + logger.info(`清理了 ${cleanedCount} 个过期缓存文件`); + } else { + logger.info(`缓存清理完成,成功删除 ${cleanedCount} 个文件,失败 ${errorCount} 个文件,跳过 ${skippedCount} 个被占用文件`); + } } } catch (error) { logger.error('清理过期缓存失败:', error); } } + /** + * 检查是否有活跃的下载任务 + */ + hasActiveDownloads() { + // 这里可以集成任务管理器来检查下载状态 + // 暂时返回false,避免过于复杂的依赖 + return false; + } + + /** + * 智能清理过期缓存 + * 避免在下载过程中清理 + */ + async smartCleanupExpiredCache() { + // 检查是否有活跃下载 + if (this.hasActiveDownloads()) { + logger.info('检测到活跃下载任务,跳过缓存清理'); + return; + } + + // 执行正常的清理 + await this.cleanupExpiredCache(); + } + /** * 启动定期清理任务 */ startCleanupTask() { setInterval(() => { - this.cleanupExpiredCache().catch(error => { + this.smartCleanupExpiredCache().catch(error => { logger.error('定期清理任务失败:', error); }); }, this.config.cleanupInterval); @@ -421,16 +469,15 @@ class ImageCacheService { // 使用索引清理所有文件 for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) { const filePath = path.join(this.cacheDir, fileInfo.filename); - try { - await fs.unlink(filePath); + + // 使用改进的安全删除方法 + const deleteSuccess = await FileUtils.safeDeleteFile(filePath); + if (deleteSuccess) { deletedCount++; - } catch (error) { - if (error.code === 'ENOENT') { - logger.warn(`清理缓存时文件不存在: ${filePath}`); - } else { - logger.error(`删除缓存文件失败: ${filePath}`, error); - errorCount++; - } + logger.debug(`成功删除缓存文件: ${filePath}`); + } else { + errorCount++; + logger.debug(`删除缓存文件失败: ${filePath}`); } } @@ -441,7 +488,7 @@ class ImageCacheService { if (errorCount === 0) { logger.info(`所有缓存已清理,共删除 ${deletedCount} 个文件`); } else { - logger.warn(`缓存清理完成,成功删除 ${deletedCount} 个文件,失败 ${errorCount} 个文件`); + logger.info(`缓存清理完成,成功删除 ${deletedCount} 个文件,失败 ${errorCount} 个文件`); } } catch (error) { logger.error('清理所有缓存失败:', error); diff --git a/backend/utils/file-utils.js b/backend/utils/file-utils.js index 3834ee7..5422d9e 100644 --- a/backend/utils/file-utils.js +++ b/backend/utils/file-utils.js @@ -11,27 +11,58 @@ const logger = defaultLogger.child('FileUtils'); */ class FileUtils { /** - * 安全删除文件(兼容 pkg 打包) + * 安全删除文件(兼容 pkg 打包,增强Windows权限处理) */ static async safeDeleteFile(filePath) { try { - // 首先尝试使用 fs-extra - if (await fs.pathExists(filePath)) { - await fs.remove(filePath); + // 首先检查文件是否存在 + if (!(await fs.pathExists(filePath))) { + logger.debug(`文件不存在,无需删除: ${filePath}`); return true; } + + // 尝试删除文件 + await fs.remove(filePath); + logger.debug(`文件删除成功: ${filePath}`); + return true; } catch (error) { - try { - // 降级到原生 fs - const nativeFs = require('fs').promises; - await nativeFs.unlink(filePath); - return true; - } catch (nativeError) { - logger.error(`文件删除失败: ${filePath}`, nativeError.message); - return false; + // 如果是权限错误,尝试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; + } + } else { + logger.warn(`删除文件权限不足: ${filePath}`, error.message); + return false; + } } + + // 其他错误类型 + if (error.code === 'ENOENT') { + logger.debug(`文件不存在,删除成功: ${filePath}`); + return true; + } + + logger.warn(`删除文件失败: ${filePath}`, error.message); + return false; } - return false; } /**