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