diff --git a/backend/services/download-executor.js b/backend/services/download-executor.js index 639bcd8..6c144a4 100644 --- a/backend/services/download-executor.js +++ b/backend/services/download-executor.js @@ -16,6 +16,9 @@ class DownloadExecutor { this.progressManager = progressManager; this.historyManager = historyManager; this.downloadService = downloadService; // 添加下载服务引用 + + // 存储每个任务的中断控制器 + this.abortControllers = new Map(); } /** @@ -23,6 +26,10 @@ class DownloadExecutor { */ async executeArtworkDownload(task, images, size, artworkDir, artwork) { try { + // 为这个任务创建中断控制器 + const abortController = new AbortController(); + this.abortControllers.set(task.id, abortController); + const results = []; for (let index = 0; index < images.length; index++) { @@ -33,6 +40,8 @@ class DownloadExecutor { // 检查是否应该暂停 if (this.shouldPause(task.id)) { logger.info('任务已暂停,停止下载:', task.id); + // 中断当前下载 + abortController.abort(); // 确保任务状态为暂停 task.status = 'paused'; await this.taskManager.saveTasks(); @@ -100,8 +109,8 @@ class DownloadExecutor { // 确保目录存在 await this.fileManager.ensureDirectory(path.dirname(filePath)); - // 下载文件并等待完成 - await this.fileManager.downloadFile(imageUrl, filePath); + // 下载文件并等待完成,传入中断控制器 + await this.fileManager.downloadFile(imageUrl, filePath, abortController); // 验证下载的文件完整性,传入期望的MIME类型 const expectedMimeType = this.getMimeTypeFromUrl(imageUrl); @@ -232,6 +241,9 @@ class DownloadExecutor { task.end_time = new Date(); await this.taskManager.saveTasks(); this.progressManager.notifyProgressUpdate(task.id, task); + } finally { + // 清理中断控制器 + this.abortControllers.delete(task.id); } } @@ -612,6 +624,19 @@ class DownloadExecutor { return { success: true }; } + /** + * 中断指定任务的下载 + * @param {string} taskId - 任务ID + */ + abortTask(taskId) { + const abortController = this.abortControllers.get(taskId); + if (abortController) { + logger.info('中断任务下载', { taskId }); + abortController.abort(); + this.abortControllers.delete(taskId); + } + } + /** * 检查任务是否应该暂停 */ diff --git a/backend/services/download.js b/backend/services/download.js index a4f0e66..ccc33fc 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -239,6 +239,9 @@ class DownloadService { // 更新任务状态为取消中,防止并发操作 await this.taskManager.updateTask(taskId, { status: 'cancelling' }); + // 立即中断正在进行的下载 + this.downloadExecutor.abortTask(taskId); + // 清理未完成的文件 await this.cleanupIncompleteFiles(task); @@ -288,6 +291,9 @@ class DownloadService { // 更新任务状态为暂停中,防止并发操作 await this.taskManager.updateTask(taskId, { status: 'pausing' }); + // 立即中断正在进行的下载 + this.downloadExecutor.abortTask(taskId); + // 清理未完成的文件 await this.cleanupIncompleteFiles(task); diff --git a/backend/services/file-manager.js b/backend/services/file-manager.js index c2f6b9e..cff3a58 100644 --- a/backend/services/file-manager.js +++ b/backend/services/file-manager.js @@ -262,7 +262,7 @@ class FileManager { /** * 简单的文件下载方法 */ - async downloadFile(url, filePath) { + async downloadFile(url, filePath, abortController = null) { const downloadConfig = await this.getDownloadConfig(); const maxRetries = downloadConfig.retryAttempts; let lastError = null; @@ -272,6 +272,11 @@ class FileManager { let response = null; try { + // 检查是否已被中断 + if (abortController && abortController.signal.aborted) { + throw new Error('下载已被中断'); + } + // 使用增强的文件工具类确保目录存在 const dirPath = path.dirname(filePath); const dirCreated = await FileUtils.safeEnsureDirEnhanced(dirPath); @@ -288,6 +293,11 @@ class FileManager { } } + // 再次检查是否已被中断 + if (abortController && abortController.signal.aborted) { + throw new Error('下载已被中断'); + } + response = await axios({ method: 'GET', url: url, @@ -296,7 +306,8 @@ class FileManager { 'Referer': 'https://www.pixiv.net/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }, - timeout: downloadConfig.timeout + timeout: downloadConfig.timeout, + signal: abortController ? abortController.signal : undefined }); // 使用增强的写入流创建方法 @@ -315,6 +326,24 @@ class FileManager { } }; + // 监听中断信号 + if (abortController) { + abortController.signal.addEventListener('abort', () => { + if (isResolved) return; + isResolved = true; + + logger.info(`下载被中断: ${filePath}`); + cleanup(); + + // 删除未完成的文件 + this.safeDeleteFile(filePath).catch(error => { + logger.warn('删除被中断的文件失败', { filePath, error: error.message }); + }); + + reject(new Error('下载被中断')); + }); + } + writer.on('finish', async () => { if (isResolved) return; isResolved = true; @@ -378,6 +407,9 @@ class FileManager { // 清理超时定时器 writer.on('finish', () => clearTimeout(timeout)); writer.on('error', () => clearTimeout(timeout)); + if (abortController) { + abortController.signal.addEventListener('abort', () => clearTimeout(timeout)); + } }); } catch (error) { @@ -391,7 +423,25 @@ class FileManager { lastError = error; - // 处理文件系统错误 + // 首先检查是否是中断错误,如果是则直接抛出,不重试 + if (error.message === '下载已被中断' || + error.code === 'ERR_CANCELED' || + error.name === 'AbortError' || + (error.message && error.message.includes('canceled'))) { + + logger.error(`下载文件失败 (尝试 ${attempt}/${maxRetries}): ${filePath}`, { + error: error.message, + stack: error.stack, + url, + retryable: false, + attempt, + reason: 'download_interrupted' + }); + + throw error; + } + + // 处理其他文件系统错误 const errorResult = ErrorHandler.handleFileSystemError(error, filePath, 'download'); logger.error(`下载文件失败 (尝试 ${attempt}/${maxRetries}): ${filePath}`, {