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