修复批量下载不添加注册表的问题,增加下载系统鲁棒性

This commit is contained in:
2025-10-03 10:08:29 +08:00
parent 16f60b4b77
commit 6508d2c438
10 changed files with 933 additions and 63 deletions
+3 -1
View File
@@ -63,7 +63,9 @@ function loggerMiddleware(req, res, next) {
req.path === '/api/download/tasks/active' ||
req.path === '/api/download/tasks/summary' ||
req.path === '/api/download/tasks/changes' ||
req.path === '/api/download/tasks/completed';
req.path === '/api/download/tasks/completed' ||
req.path === '/api/download/registry/stats' ||
/^\/api\/download\/check\/\d+/.test(req.path);
// 过滤掉仓库预览请求(图片预览)
const isRepositoryPreview = req.path === '/api/repository/preview';
+75
View File
@@ -439,6 +439,81 @@ router.post('/resume/:taskId', async (req, res) => {
}
});
/**
* 暂停批量下载任务
* POST /api/download/batch/pause/:taskId
*/
router.post('/batch/pause/:taskId', async (req, res) => {
try {
const { taskId } = req.params;
if (!taskId) {
return res.status(400).json({
success: false,
error: 'Task ID is required'
});
}
const downloadService = req.backend.getDownloadService();
const result = await downloadService.pauseBatchTask(taskId);
if (result.success) {
res.json({
success: true,
message: '批量下载任务已暂停'
});
} else {
res.status(400).json({
success: false,
error: result.error
});
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* 恢复批量下载任务
* POST /api/download/batch/resume/:taskId
*/
router.post('/batch/resume/:taskId', async (req, res) => {
try {
const { taskId } = req.params;
if (!taskId) {
return res.status(400).json({
success: false,
error: 'Task ID is required'
});
}
const downloadService = req.backend.getDownloadService();
const result = await downloadService.resumeBatchTask(taskId);
if (result.success) {
res.json({
success: true,
data: result.data,
message: '批量下载任务已恢复'
});
} else {
res.status(400).json({
success: false,
error: result.error
});
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* 取消任务
* DELETE /api/download/cancel/:taskId
+159 -22
View File
@@ -76,8 +76,9 @@ class DownloadExecutor {
// 检查文件是否已存在且完整
if (await this.fileManager.fileExists(filePath)) {
// 验证文件完整性
const integrity = await this.fileManager.checkFileIntegrity(filePath);
// 验证文件完整性,传入期望的MIME类型
const expectedMimeType = this.getMimeTypeFromUrl(imageUrl);
const integrity = await this.fileManager.checkFileIntegrity(filePath, null, expectedMimeType);
if (integrity.valid) {
// 只有在非恢复模式下才增加计数,避免重复计算
if (!task.isResuming) {
@@ -90,7 +91,7 @@ class DownloadExecutor {
continue;
} else {
// 文件不完整,删除重新下载
logger.info(`文件不完整,重新下载: ${filePath}`);
logger.info(`文件不完整,重新下载: ${filePath}, 原因: ${integrity.reason}`);
await this.fileManager.safeDeleteFile(filePath);
}
}
@@ -102,9 +103,12 @@ class DownloadExecutor {
// 下载文件并等待完成
await this.fileManager.downloadFile(imageUrl, filePath);
// 验证下载的文件完整性
const integrity = await this.fileManager.checkFileIntegrity(filePath);
// 验证下载的文件完整性,传入期望的MIME类型
const expectedMimeType = this.getMimeTypeFromUrl(imageUrl);
const integrity = await this.fileManager.checkFileIntegrity(filePath, null, expectedMimeType);
if (!integrity.valid) {
// 删除损坏的文件
await this.fileManager.safeDeleteFile(filePath);
throw new Error(`文件下载不完整: ${integrity.reason}`);
}
@@ -116,9 +120,28 @@ class DownloadExecutor {
results.push({ success: true, file: fileName });
} catch (error) {
task.failed_files++;
logger.error(`下载图片失败 ${index + 1}: ${error.message}`);
logger.error(`下载图片失败 ${index + 1}: ${error.message}`, {
taskId: task.id,
imageUrl,
filePath,
error: error.stack
});
// 尝试清理可能存在的损坏文件
try {
if (await this.fileManager.fileExists(filePath)) {
await this.fileManager.safeDeleteFile(filePath);
logger.debug('已清理损坏的文件', { filePath });
}
} catch (cleanupError) {
logger.warn('清理损坏文件失败', {
filePath,
error: cleanupError.message
});
}
this.progressManager.notifyProgressUpdate(task.id, task);
results.push({ success: false, error: error.message });
results.push({ success: false, error: error.message, file: fileName });
}
}
@@ -139,16 +162,49 @@ class DownloadExecutor {
await this.taskManager.saveTasks();
this.progressManager.notifyProgressUpdate(task.id, task);
// 如果下载成功,更新下载注册表
if (task.status === 'completed') {
try {
await this.downloadService.downloadRegistry.addArtwork(task.artist_name, task.artwork_id);
logger.debug('已更新下载注册表', {
artistName: task.artist_name,
artworkId: task.artwork_id
// 只有在所有文件都成功下载且完整性检查通过时,才更新下载注册表
if (task.status === 'completed' && task.failed_files === 0 && task.completed_files === task.total_files) {
// 再次验证所有文件的完整性
let allFilesValid = true;
const artworkFiles = await fs.readdir(artworkDir);
const imageFiles = artworkFiles.filter(file =>
file.startsWith('image_') &&
!file.endsWith('.json') &&
!file.endsWith('.txt')
);
for (const imageFile of imageFiles) {
const filePath = path.join(artworkDir, imageFile);
const expectedMimeType = this.getMimeTypeFromUrl(imageFile);
const integrity = await this.fileManager.checkFileIntegrity(filePath, null, expectedMimeType);
if (!integrity.valid) {
allFilesValid = false;
logger.warn('发现不完整文件,不添加到下载注册表', {
file: imageFile,
reason: integrity.reason
});
break;
}
}
if (allFilesValid && imageFiles.length === task.total_files) {
try {
await this.downloadService.downloadRegistry.addArtwork(task.artist_name, task.artwork_id);
logger.debug('已更新下载注册表', {
artistName: task.artist_name,
artworkId: task.artwork_id,
totalFiles: task.total_files,
completedFiles: task.completed_files
});
} catch (error) {
logger.warn('更新下载注册表失败:', error.message);
}
} else {
logger.warn('文件完整性验证失败或文件数量不匹配,不添加到下载注册表', {
expectedFiles: task.total_files,
actualFiles: imageFiles.length,
allFilesValid
});
} catch (error) {
logger.warn('更新下载注册表失败:', error.message);
}
}
@@ -218,6 +274,16 @@ class DownloadExecutor {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map(async item => {
try {
// 检查是否应该暂停(在每个作品下载前检查)
if (this.shouldPause(task.id)) {
logger.info('批量下载任务已暂停,停止当前作品下载:', task.id);
// 设置任务状态为暂停
task.status = 'paused';
await this.taskManager.saveTasks();
this.progressManager.notifyProgressUpdate(task.id, task);
return { artwork_id: typeof item === 'object' ? item.id : item, success: false, paused: true };
}
// 获取作品ID - 支持直接传入ID或作品对象
const artworkId = typeof item === 'object' ? item.id : item;
@@ -237,9 +303,30 @@ class DownloadExecutor {
results.push(result);
return result;
} else {
// 真正下载成功
// 真正下载成功,立即添加到注册表
task.completed_files++;
// 立即添加到下载注册表
try {
await this.downloadService.downloadRegistry.addArtwork(
downloadResult.artist_name,
artworkId
);
logger.debug(`批量下载中的作品 ${artworkId} 已添加到下载注册表`, {
artworkId,
artistName: downloadResult.artist_name,
taskId: task.id
});
} catch (error) {
logger.error(`批量下载中添加作品到注册表失败: ${artworkId}`, {
artworkId,
artistName: downloadResult.artist_name,
taskId: task.id,
error: error.message,
stack: error.stack
});
}
// 添加到最近完成列表
const completedItem = {
artwork_id: artworkId,
@@ -294,6 +381,12 @@ class DownloadExecutor {
}
}
// 检查任务是否被暂停,如果是则不要更新最终状态
if (task.status === 'paused') {
logger.info('批量下载任务已暂停,跳过最终状态更新:', task.id);
return;
}
// 更新任务状态
task.status = task.failed_files === 0 ? 'completed' : 'partial';
task.end_time = new Date();
@@ -328,6 +421,30 @@ class DownloadExecutor {
}
}
/**
* 根据URL获取MIME类型
* @param {string} url - 图片URL
* @returns {string} MIME类型
*/
getMimeTypeFromUrl(url) {
const ext = this.getFileExtension(url).toLowerCase();
switch (ext) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'webp':
return 'image/webp';
case 'bmp':
return 'image/bmp';
default:
return 'image/jpeg'; // 默认为JPEG
}
}
/**
* 获取文件扩展名
*/
@@ -423,14 +540,15 @@ class DownloadExecutor {
// 检查文件是否存在且完整
if (await this.fileManager.fileExists(filePath)) {
const integrity = await this.fileManager.checkFileIntegrity(filePath);
const expectedMimeType = this.getMimeTypeFromUrl(imageUrl);
const integrity = await this.fileManager.checkFileIntegrity(filePath, null, expectedMimeType);
if (integrity.valid) {
completedFiles.push({ index, fileName, filePath });
} else {
incompleteFiles.push({ index, fileName, filePath });
incompleteFiles.push({ index, fileName, filePath, reason: integrity.reason });
}
} else {
incompleteFiles.push({ index, fileName, filePath });
incompleteFiles.push({ index, fileName, filePath, reason: '文件不存在' });
}
}
@@ -466,9 +584,28 @@ class DownloadExecutor {
await this.executeArtworkDownload(task, images, 'original', artworkDir, artwork);
} else if (task.type === 'batch' || task.type === 'artist') {
// 批量下载和作者下载的恢复逻辑
// 这里需要根据具体实现来恢复
logger.info('恢复批量下载任务:', taskId);
// TODO: 实现批量下载的恢复逻辑
// 重置任务状态为下载中
task.status = 'downloading';
task.isResuming = true;
await this.taskManager.saveTasks();
this.progressManager.notifyProgressUpdate(task.id, task);
// 获取原始的作品列表
const items = task.items || [];
if (items.length === 0) {
logger.error('批量下载任务没有作品列表,无法恢复', { taskId });
throw new Error('批量下载任务没有作品列表,无法恢复');
}
// 重新开始批量下载
await this.executeBatchDownload(task, items, {
size: task.size || 'original',
quality: task.quality || 'high',
format: task.format || 'auto',
concurrent: task.concurrent || 3
});
}
logger.info('任务恢复执行完成', { taskId });
+453 -17
View File
@@ -233,13 +233,42 @@ class DownloadService {
return { success: false, error: '任务不存在' };
}
await this.taskManager.updateTask(taskId, {
status: 'cancelled',
end_time: new Date(),
});
logger.info('开始取消任务', { taskId, status: task.status, type: task.type });
this.progressManager.notifyProgressUpdate(taskId, task);
return { success: true };
try {
// 更新任务状态为取消中,防止并发操作
await this.taskManager.updateTask(taskId, { status: 'cancelling' });
// 清理未完成的文件
await this.cleanupIncompleteFiles(task);
// 最终更新任务状态
await this.taskManager.updateTask(taskId, {
status: 'cancelled',
end_time: new Date(),
});
// 获取更新后的任务并通知
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
logger.info('任务取消完成', { taskId });
return { success: true };
} catch (error) {
logger.error('取消任务失败', { taskId, error: error.message });
// 如果清理失败,仍然标记为取消,但记录错误
await this.taskManager.updateTask(taskId, {
status: 'cancelled',
end_time: new Date(),
error: `取消时清理失败: ${error.message}`
});
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
return { success: true, warning: `任务已取消,但清理时出现问题: ${error.message}` };
}
}
async pauseTask(taskId) {
@@ -253,13 +282,38 @@ class DownloadService {
return { success: false, error: '只能暂停正在下载的任务' };
}
await this.taskManager.updateTask(taskId, { status: 'paused' });
// 获取更新后的任务
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
return { success: true, data: updatedTask };
logger.info('开始暂停任务', { taskId, status: task.status, type: task.type });
try {
// 更新任务状态为暂停中,防止并发操作
await this.taskManager.updateTask(taskId, { status: 'pausing' });
// 清理未完成的文件
await this.cleanupIncompleteFiles(task);
// 最终更新任务状态为暂停
await this.taskManager.updateTask(taskId, { status: 'paused' });
// 获取更新后的任务
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
logger.info('任务暂停完成', { taskId });
return { success: true, data: updatedTask };
} catch (error) {
logger.error('暂停任务失败', { taskId, error: error.message });
// 如果清理失败,仍然标记为暂停,但记录错误
await this.taskManager.updateTask(taskId, {
status: 'paused',
error: `暂停时清理失败: ${error.message}`
});
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
return { success: true, data: updatedTask, warning: `任务已暂停,但清理时出现问题: ${error.message}` };
}
}
async resumeTask(taskId) {
@@ -311,6 +365,288 @@ class DownloadService {
return { success: true, data: this.taskManager.getTask(taskId) };
}
/**
* 暂停批量下载任务
*/
async pauseBatchTask(taskId) {
const task = this.taskManager.getTask(taskId);
if (!task) {
return { success: false, error: '任务不存在' };
}
// 检查是否为批量下载任务
if (!['batch', 'artist', 'art'].includes(task.type)) {
return { success: false, error: '此方法仅适用于批量下载任务' };
}
// 只允许暂停正在下载的任务
if (task.status !== 'downloading') {
return { success: false, error: '只能暂停正在下载的任务' };
}
logger.info('开始暂停批量下载任务', { taskId, status: task.status, type: task.type });
try {
// 直接设置任务状态为暂停,不进行文件清理
// 批量下载中的每个文件都是独立完成的,不需要清理
await this.taskManager.updateTask(taskId, { status: 'paused' });
// 获取更新后的任务
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
logger.info('批量下载任务暂停完成', { taskId });
return { success: true, data: updatedTask };
} catch (error) {
logger.error('暂停批量下载任务失败', { taskId, error: error.message });
return { success: false, error: `暂停任务失败: ${error.message}` };
}
}
/**
* 恢复批量下载任务
*/
async resumeBatchTask(taskId) {
const task = this.taskManager.getTask(taskId);
if (!task) {
logger.error('恢复批量下载任务失败:任务不存在', { taskId });
return { success: false, error: '任务不存在' };
}
// 检查是否为批量下载任务
if (!['batch', 'artist', 'art'].includes(task.type)) {
return { success: false, error: '此方法仅适用于批量下载任务' };
}
// 只允许恢复暂停的任务
if (task.status !== 'paused') {
logger.warn('恢复批量下载任务失败:任务状态不是暂停状态', {
taskId,
currentStatus: task.status
});
return { success: false, error: '只能恢复暂停的任务' };
}
// 重新开始批量下载执行
try {
logger.info('开始恢复批量下载任务执行', { taskId });
// 直接设置任务状态为下载中
await this.taskManager.updateTask(taskId, { status: 'downloading' });
// 获取原始的作品列表
const items = task.items || [];
if (items.length === 0) {
logger.error('批量下载任务没有作品列表,无法恢复', { taskId });
await this.taskManager.updateTask(taskId, { status: 'paused' });
return { success: false, error: '批量下载任务没有作品列表,无法恢复' };
}
// 通知状态更新
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
// 异步重新开始批量下载,不等待完成
setImmediate(async () => {
try {
await this.downloadExecutor.executeBatchDownload(task, items, {
size: task.size || 'original',
quality: task.quality || 'high',
format: task.format || 'auto',
concurrent: task.concurrent || 3
});
} catch (error) {
logger.error('批量下载任务恢复执行失败', {
taskId,
error: error.message,
stack: error.stack
});
// 如果执行失败,设置任务状态为失败
await this.taskManager.updateTask(taskId, {
status: 'failed',
error: error.message
});
const failedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, failedTask);
}
});
logger.info('批量下载任务恢复成功', {
taskId,
newStatus: updatedTask.status
});
return { success: true, data: updatedTask };
} catch (error) {
logger.error('恢复批量下载任务失败', {
taskId,
error: error.message,
stack: error.stack
});
// 如果恢复失败,保持暂停状态
await this.taskManager.updateTask(taskId, { status: 'paused' });
return { success: false, error: `恢复任务失败: ${error.message}` };
}
}
/**
* 清理未完成的文件
* @param {Object} task - 任务对象
*/
async cleanupIncompleteFiles(task) {
if (!task) {
logger.warn('清理未完成文件:任务对象为空');
return;
}
logger.info('开始清理未完成文件', { taskId: task.id, type: task.type });
try {
if (task.type === 'artwork') {
await this.cleanupArtworkIncompleteFiles(task);
} else if (task.type === 'batch' || task.type === 'artist') {
// 批量下载任务通常不需要清理单个文件,因为每个作品都是独立处理的
logger.info('批量任务无需清理单个文件', { taskId: task.id, type: task.type });
}
} catch (error) {
logger.error('清理未完成文件失败', { taskId: task.id, error: error.message });
throw error;
}
}
/**
* 清理单个作品任务的未完成文件
* @param {Object} task - 作品下载任务
*/
async cleanupArtworkIncompleteFiles(task) {
if (!task.artwork_id || !task.artist_name || !task.artwork_title) {
logger.warn('作品任务信息不完整,跳过文件清理', {
taskId: task.id,
artwork_id: task.artwork_id,
artist_name: task.artist_name,
artwork_title: task.artwork_title
});
return;
}
try {
// 构建作品目录路径
const downloadPath = await this.fileManager.getDownloadPath();
const artistName = this.fileManager.createSafeDirectoryName(task.artist_name);
const artworkTitle = this.fileManager.createSafeDirectoryName(task.artwork_title);
const artistDir = path.join(downloadPath, artistName);
const artworkDirName = `${task.artwork_id}_${artworkTitle}`;
const artworkDir = path.join(artistDir, artworkDirName);
// 检查作品目录是否存在
if (!(await this.fileManager.directoryExists(artworkDir))) {
logger.debug('作品目录不存在,无需清理', { taskId: task.id, artworkDir });
return;
}
// 获取目录中的所有文件
const files = await this.fileManager.listDirectory(artworkDir);
const imageFiles = files.filter(file =>
/\.(jpg|jpeg|png|gif|webp)$/i.test(file) && file !== 'artwork_info.json'
);
let cleanedCount = 0;
let errorCount = 0;
// 检查并删除未完成的图片文件
for (const fileName of imageFiles) {
const filePath = path.join(artworkDir, fileName);
try {
// 检查文件完整性,根据文件扩展名推断MIME类型
const expectedMimeType = this.getMimeTypeFromExtension(fileName);
const integrity = await this.fileManager.checkFileIntegrity(filePath, null, expectedMimeType);
if (!integrity.valid) {
// 文件不完整,删除它
const deleted = await this.fileManager.safeDeleteFile(filePath);
if (deleted) {
cleanedCount++;
logger.debug('删除未完成文件', {
taskId: task.id,
fileName,
reason: integrity.reason
});
} else {
errorCount++;
logger.warn('删除未完成文件失败', {
taskId: task.id,
fileName,
reason: '文件可能被占用'
});
}
}
} catch (error) {
errorCount++;
logger.warn('检查文件完整性失败', {
taskId: task.id,
fileName,
error: error.message
});
}
}
// 如果目录中只剩下artwork_info.json或为空,删除整个目录
const remainingFiles = await this.fileManager.listDirectory(artworkDir);
const remainingImageFiles = remainingFiles.filter(file =>
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
);
if (remainingImageFiles.length === 0) {
try {
await this.fileManager.removeDirectory(artworkDir);
logger.info('删除空的作品目录', { taskId: task.id, artworkDir });
} catch (error) {
logger.warn('删除空目录失败', {
taskId: task.id,
artworkDir,
error: error.message
});
}
}
logger.info('文件清理完成', {
taskId: task.id,
cleanedCount,
errorCount,
totalImageFiles: imageFiles.length
});
} catch (error) {
logger.error('清理作品文件失败', { taskId: task.id, error: error.message });
throw error;
}
}
/**
* 根据文件扩展名获取MIME类型
* @param {string} fileName - 文件名
* @returns {string} MIME类型
*/
getMimeTypeFromExtension(fileName) {
const ext = path.extname(fileName).toLowerCase().replace('.', '');
switch (ext) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'webp':
return 'image/webp';
case 'bmp':
return 'image/bmp';
default:
return 'image/jpeg'; // 默认为JPEG
}
}
// 代理方法 - 历史记录管理
async getDownloadHistory(offset = 0, limit = 50) {
const result = this.historyManager.getDownloadHistory(offset, limit);
@@ -651,7 +987,12 @@ class DownloadService {
},
};
} catch (error) {
logger.error('下载作品失败:', error);
logger.error('下载作品失败:', {
artworkId,
error: error.message,
stack: error.stack,
options
});
return {
success: false,
error: error.message,
@@ -687,9 +1028,27 @@ class DownloadService {
try {
// 检查是否已下载
if (skipExisting && (await this.isArtworkDownloaded(artworkId))) {
// 获取作品信息用于返回
let artistName = 'Unknown Artist';
let artworkTitle = 'Untitled';
try {
const artworkResult = await this.artworkService.getArtworkDetail(artworkId);
if (artworkResult.success && artworkResult.data) {
const artwork = artworkResult.data;
artistName = this.fileManager.createSafeDirectoryName(artwork.user?.name || 'Unknown Artist');
artworkTitle = this.fileManager.createSafeDirectoryName(artwork.title || 'Untitled');
}
} catch (error) {
logger.debug(`获取已下载作品信息失败: ${artworkId}`, error.message);
}
return {
success: true,
skipped: true,
artwork_id: artworkId,
artist_name: artistName,
artwork_title: artworkTitle,
message: '作品已存在且完整,跳过下载',
};
}
@@ -779,7 +1138,14 @@ class DownloadService {
await this.fileManager.downloadFile(imageUrl, filePath);
results.push({ success: true, file: fileName });
} catch (error) {
logger.error(`下载图片失败 ${index + 1}: ${error.message}`);
logger.error(`下载图片失败 ${index + 1}:`, {
artworkId,
imageIndex: index + 1,
imageUrl,
filePath,
error: error.message,
stack: error.stack
});
results.push({ success: false, error: error.message });
}
}
@@ -791,9 +1157,73 @@ class DownloadService {
// 检查下载结果
const failedCount = results.filter(r => !r.success).length;
const successCount = results.filter(r => r.success && !r.skipped).length;
const skippedCount = results.filter(r => r.success && r.skipped).length;
// 只有在所有文件都成功下载(包括跳过的文件)时才添加到注册表
const allFilesSuccessful = failedCount === 0;
if (allFilesSuccessful) {
try {
// 执行文件完整性检查
let integrityCheckPassed = true;
for (let index = 0; index < images.length; index++) {
const fileName = `image_${index + 1}.${this.getFileExtension(images[index].original || images[index].large || images[index].medium)}`;
const filePath = path.join(artworkDir, fileName);
if (await this.fileManager.fileExists(filePath)) {
// 检查文件大小
const stats = await fs.stat(filePath);
if (stats.size === 0) {
logger.warn(`文件大小为0,完整性检查失败: ${filePath}`);
integrityCheckPassed = false;
break;
}
// 检查MIME类型 - 使用checkFileHeader方法来检测文件类型
const headerCheck = await this.fileManager.checkFileHeader(filePath);
if (!headerCheck.valid || !headerCheck.detectedType || !headerCheck.detectedType.startsWith('image/')) {
logger.warn(`文件MIME类型检查失败: ${filePath}, 检测结果: ${JSON.stringify(headerCheck)}`);
integrityCheckPassed = false;
break;
}
}
}
if (integrityCheckPassed) {
// 添加到下载注册表(仅用于单个作品下载,批量下载在executeBatchDownload中处理)
await this.downloadRegistry.addArtwork(artistName, artworkId);
logger.debug(`作品 ${artworkId} 已添加到下载注册表`, {
artworkId,
artistName,
totalFiles: images.length,
completedFiles: successCount,
skippedFiles: skippedCount
});
} else {
logger.warn(`作品 ${artworkId} 文件完整性检查失败,未添加到下载注册表`, {
artworkId,
artistName
});
}
} catch (error) {
logger.error(`添加作品到下载注册表失败: ${artworkId}`, {
artworkId,
artistName,
error: error.message,
stack: error.stack
});
}
} else {
logger.debug(`作品 ${artworkId} 下载不完整,未添加到下载注册表`, {
artworkId,
artistName,
failedCount,
totalFiles: images.length
});
}
return {
success: failedCount === 0,
success: allFilesSuccessful,
artwork_id: artworkId,
artist_name: artistName,
artwork_title: artworkTitle,
@@ -803,7 +1233,12 @@ class DownloadService {
results: results,
};
} catch (error) {
logger.error(`下载作品 ${artworkId} 失败:`, error);
logger.error(`下载作品 ${artworkId} 失败:`, {
artworkId,
error: error.message,
stack: error.stack,
options
});
return {
success: false,
error: error.message,
@@ -872,6 +1307,7 @@ class DownloadService {
results: [],
task_description: taskDescription,
task_title: taskTitle,
items: items, // 保存原始的作品列表,用于恢复任务
// 保留原有的任务特定字段
...(options.artist_id && { artist_id: options.artist_id }),
...(options.artist_name && { artist_name: options.artist_name }),
+162 -9
View File
@@ -111,7 +111,14 @@ class FileManager {
/**
* 检查文件完整性
*/
async checkFileIntegrity(filePath, expectedSize = null) {
/**
* 检查文件完整性
* @param {string} filePath - 文件路径
* @param {number} expectedSize - 期望的文件大小
* @param {string} expectedMimeType - 期望的MIME类型
* @returns {Object} 检查结果
*/
async checkFileIntegrity(filePath, expectedSize = null, expectedMimeType = null) {
try {
if (!await fs.pathExists(filePath)) {
return { valid: false, reason: '文件不存在' };
@@ -130,8 +137,15 @@ class FileManager {
}
// 检查文件是否过小(可能下载不完整)
if (stats.size < 512) { // 小于512字节的文件可能是损坏的
return { valid: false, reason: '文件过小,可能下载不完整', size: stats.size };
const minSize = this.getMinimumFileSize(filePath, expectedMimeType);
if (stats.size < minSize) {
return { valid: false, reason: '文件过小,可能下载不完整', size: stats.size, minSize };
}
// 检查文件头部是否符合预期格式
const headerCheck = await this.checkFileHeader(filePath, expectedMimeType);
if (!headerCheck.valid) {
return headerCheck;
}
// 文件存在且大小正常,认为有效
@@ -141,6 +155,110 @@ class FileManager {
}
}
/**
* 获取文件的最小合理大小
* @param {string} filePath - 文件路径
* @param {string} expectedMimeType - 期望的MIME类型
* @returns {number} 最小文件大小
*/
getMinimumFileSize(filePath, expectedMimeType) {
const ext = path.extname(filePath).toLowerCase();
// 根据文件类型设置最小大小
if (expectedMimeType) {
if (expectedMimeType.startsWith('image/')) {
return 1024; // 图片文件至少1KB
}
}
// 根据扩展名判断
switch (ext) {
case '.jpg':
case '.jpeg':
return 1024; // JPEG文件至少1KB
case '.png':
return 512; // PNG文件至少512字节
case '.gif':
return 256; // GIF文件至少256字节
case '.webp':
return 512; // WebP文件至少512字节
case '.bmp':
return 1024; // BMP文件至少1KB
default:
return 256; // 其他文件至少256字节
}
}
/**
* 检查文件头部格式
* @param {string} filePath - 文件路径
* @param {string} expectedMimeType - 期望的MIME类型
* @returns {Object} 检查结果
*/
async checkFileHeader(filePath, expectedMimeType) {
try {
// 读取文件前几个字节来检查文件头
const buffer = Buffer.alloc(16);
const fd = await fs.open(filePath, 'r');
try {
const { bytesRead } = await fs.read(fd, buffer, 0, 16, 0);
if (bytesRead < 4) {
return { valid: false, reason: '文件头部数据不足' };
}
// 检查常见图片格式的文件头
const header = buffer.toString('hex', 0, Math.min(bytesRead, 8));
// JPEG文件头: FFD8FF
if (header.startsWith('ffd8ff')) {
if (expectedMimeType && !expectedMimeType.includes('jpeg') && !expectedMimeType.includes('jpg')) {
return { valid: false, reason: '文件格式不匹配:检测到JPEG但期望其他格式' };
}
return { valid: true, detectedType: 'image/jpeg' };
}
// PNG文件头: 89504E47
if (header.startsWith('89504e47')) {
if (expectedMimeType && !expectedMimeType.includes('png')) {
return { valid: false, reason: '文件格式不匹配:检测到PNG但期望其他格式' };
}
return { valid: true, detectedType: 'image/png' };
}
// GIF文件头: 474946
if (header.startsWith('474946')) {
if (expectedMimeType && !expectedMimeType.includes('gif')) {
return { valid: false, reason: '文件格式不匹配:检测到GIF但期望其他格式' };
}
return { valid: true, detectedType: 'image/gif' };
}
// WebP文件头: 52494646...57454250
if (header.startsWith('52494646') && buffer.toString('hex', 8, 12) === '57454250') {
if (expectedMimeType && !expectedMimeType.includes('webp')) {
return { valid: false, reason: '文件格式不匹配:检测到WebP但期望其他格式' };
}
return { valid: true, detectedType: 'image/webp' };
}
// 如果没有明确的期望类型,且检测到了有效的图片头部,则认为有效
if (!expectedMimeType) {
return { valid: true, detectedType: 'unknown' };
}
// 如果有期望类型但未匹配到已知格式,可能是损坏的文件
return { valid: false, reason: '无法识别的文件格式或文件头部损坏' };
} finally {
await fs.close(fd);
}
} catch (error) {
return { valid: false, reason: '检查文件头部失败', error: error.message };
}
}
/**
* 简单的文件下载方法
*/
@@ -217,11 +335,24 @@ class FileManager {
if (isResolved) return;
isResolved = true;
logger.error(`文件写入流错误: ${filePath}`, {
error: error.message,
stack: error.stack,
attempt,
url
});
// 下载失败时删除文件
try {
await this.safeDeleteFile(filePath);
if (await fs.pathExists(filePath)) {
await this.safeDeleteFile(filePath);
logger.debug('已清理失败的下载文件', { filePath });
}
} catch (removeError) {
logger.warn('清理失败文件时出错:', removeError.message);
logger.warn('清理失败文件时出错:', {
filePath,
error: removeError.message
});
}
cleanup();
@@ -233,6 +364,12 @@ class FileManager {
if (isResolved) return;
isResolved = true;
logger.error(`下载超时: ${filePath}`, {
url,
timeout: downloadConfig.timeout + 60000,
attempt
});
const timeoutError = new Error('下载超时');
cleanup();
reject(timeoutError);
@@ -257,7 +394,13 @@ class FileManager {
// 处理文件系统错误
const errorResult = ErrorHandler.handleFileSystemError(error, filePath, 'download');
logger.error(`下载文件失败 (尝试 ${attempt}/${maxRetries}): ${filePath}`, error.message);
logger.error(`下载文件失败 (尝试 ${attempt}/${maxRetries}): ${filePath}`, {
error: error.message,
stack: error.stack,
url,
retryable: errorResult.retryable,
attempt
});
// 如果不是可重试的错误,直接抛出
if (!errorResult.retryable) {
@@ -266,7 +409,12 @@ class FileManager {
// 如果是最后一次尝试,抛出错误
if (attempt === maxRetries) {
logger.error(`下载文件最终失败: ${filePath}`, error.message);
logger.error(`下载文件最终失败: ${filePath}`, {
error: error.message,
stack: error.stack,
url,
totalAttempts: maxRetries
});
throw error;
}
@@ -417,9 +565,14 @@ class FileManager {
try {
if (await fs.pathExists(filePath)) {
await fs.unlink(filePath);
logger.debug('文件删除成功', { filePath });
}
} catch (error) {
logger.error(`文件删除失败: ${filePath}`, error.message);
logger.error(`文件删除失败: ${filePath}`, {
error: error.message,
stack: error.stack,
code: error.code
});
// 不抛出错误,避免影响其他操作
}
}
@@ -458,4 +611,4 @@ class FileManager {
}
}
module.exports = FileManager;
module.exports = FileManager;
+6 -3
View File
@@ -20,16 +20,19 @@ class ErrorHandler {
isPkg: process.pkg !== undefined,
platform: process.platform,
errorCode: error.code,
errorMessage: error.message
errorMessage: error.message,
timestamp: new Date().toISOString()
};
// 记录错误信息
// 记录详细错误信息
logger.error(`文件系统错误 [${operation}]:`, {
filePath: filePath,
errorCode: error.code,
errorMessage: error.message,
stack: error.stack,
isPkg: errorInfo.isPkg,
platform: errorInfo.platform
platform: errorInfo.platform,
timestamp: errorInfo.timestamp
});
// 根据错误类型提供解决方案
+1 -1
View File
@@ -20,7 +20,7 @@ REM INFO: 显示一般信息及以上级别信息 (默认)
REM DEBUG: 显示调试信息及以上级别信息
REM TRACE: 显示所有级别信息 (最详细)
REM ========================================
set LOG_LEVEL=
set LOG_LEVEL=DEBUG
echo.
echo Pixiv Manager 启动中...
+14
View File
@@ -73,6 +73,20 @@ class DownloadService {
return apiService.post(`/api/download/resume/${taskId}`);
}
/**
* 暂停批量下载任务
*/
async pauseBatchTask(taskId: string) {
return apiService.post(`/api/download/batch/pause/${taskId}`);
}
/**
* 恢复批量下载任务
*/
async resumeBatchTask(taskId: string) {
return apiService.post(`/api/download/batch/resume/${taskId}`);
}
/**
* 取消任务
*/
+27 -3
View File
@@ -303,7 +303,19 @@ export const useDownloadStore = defineStore('download', () => {
// 恢复任务
const resumeTask = async (taskId: string) => {
try {
const response = await downloadService.resumeTask(taskId);
// 获取任务信息以确定类型
const task = getTask(taskId);
let response;
// 判断是否为批量下载任务(batch、artist、art类型都是批量下载)
if (task && ['batch', 'artist', 'art'].includes(task.type)) {
// 使用批量下载专用API
response = await downloadService.resumeBatchTask(taskId);
} else {
// 使用单个下载API
response = await downloadService.resumeTask(taskId);
}
if (response.success) {
await fetchTasks();
// 重新管理SSE连接
@@ -321,7 +333,19 @@ export const useDownloadStore = defineStore('download', () => {
// 暂停任务
const pauseTask = async (taskId: string) => {
try {
const response = await downloadService.pauseTask(taskId);
// 获取任务信息以确定类型
const task = getTask(taskId);
let response;
// 判断是否为批量下载任务(batch、artist、art类型都是批量下载)
if (task && ['batch', 'artist', 'art'].includes(task.type)) {
// 使用批量下载专用API
response = await downloadService.pauseBatchTask(taskId);
} else {
// 使用单个下载API
response = await downloadService.pauseTask(taskId);
}
if (response.success) {
await fetchTasks();
} else {
@@ -410,4 +434,4 @@ export const useDownloadStore = defineStore('download', () => {
startRefreshInterval,
stopRefreshInterval
};
});
});
+33 -7
View File
@@ -62,6 +62,9 @@
</span>
</div>
<div class="task-actions">
<button v-if="task.status === 'downloading'" @click="pauseTask(task.id)" class="btn btn-warning btn-sm">
暂停
</button>
<button v-if="task.status === 'downloading'" @click="cancelTask(task.id)" class="btn btn-danger btn-sm">
取消
</button>
@@ -281,11 +284,11 @@ const getHistoryTitle = (item: any) => {
const count = item.total_files || 0;
let title = '排行榜下载';
if (item.mode && item.ranking_type) {
const modeText = item.mode === 'daily' ? '日榜' :
item.mode === 'weekly' ? '周榜' :
item.mode === 'monthly' ? '月榜' : item.mode;
const typeText = item.ranking_type === 'illust' ? '插画' :
item.ranking_type === 'manga' ? '漫画' : item.ranking_type;
const modeText = item.mode === 'daily' ? '日榜' :
item.mode === 'weekly' ? '周榜' :
item.mode === 'monthly' ? '月榜' : item.mode;
const typeText = item.ranking_type === 'illust' ? '插画' :
item.ranking_type === 'manga' ? '漫画' : item.ranking_type;
title = `${modeText}${typeText}`;
}
return `${title} - ${count} 个作品`;
@@ -370,6 +373,16 @@ const resumeTask = async (taskId: string) => {
}
};
// 暂停任务
const pauseTask = async (taskId: string) => {
try {
await downloadStore.pauseTask(taskId);
} catch (err) {
error.value = err instanceof Error ? err.message : '暂停任务失败';
console.error('暂停任务失败:', err);
}
};
// 清理历史记录
const cleanupHistory = async () => {
if (confirm('确定要清理下载历史吗?这将保留最新的500条记录。')) {
@@ -783,7 +796,7 @@ onUnmounted(() => {
background-clip: text;
}
.stat-value.success + .stat-label {
.stat-value.success+.stat-label {
color: #047857;
}
@@ -799,7 +812,7 @@ onUnmounted(() => {
background-clip: text;
}
.stat-value.error + .stat-label {
.stat-value.error+.stat-label {
color: #b91c1c;
}
@@ -905,6 +918,19 @@ onUnmounted(() => {
box-shadow: 0 4px 6px -1px rgba(220, 38, 38, 0.3);
}
.btn-warning {
background: #f59e0b;
color: white;
border: 1px solid #f59e0b;
}
.btn-warning:hover:not(:disabled) {
background: #d97706;
border-color: #d97706;
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(245, 158, 11, 0.3);
}
@media (max-width: 768px) {
.container {
padding: 0 1rem;