修复缓存删除问题

This commit is contained in:
2025-10-01 19:26:34 +08:00
parent 0d294536ca
commit 194566c8fb
4 changed files with 604 additions and 148 deletions
+276 -75
View File
@@ -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<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;
}
// 如果删除失败但不是最后一次尝试,等待后重试
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;
module.exports = ImageCacheService;