From 2b700978ad3db5b4b18af82f01f16bbedfcd1afe Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Tue, 17 Feb 2026 19:32:48 +0800 Subject: [PATCH] fix: precheck local downloads to avoid JSON file download on quota errors --- backend/server.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++ frontend/app.js | 40 +++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/backend/server.js b/backend/server.js index bca76b1..9e4f28e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4813,6 +4813,67 @@ app.post('/api/upload', authMiddleware, upload.single('file'), async (req, res) } }); +// 下载预检(避免前端直接下载到 JSON 错误响应) +app.get('/api/files/download-check', authMiddleware, async (req, res) => { + const filePath = req.query.path; + let storage; + + if (!filePath) { + return res.status(400).json({ + success: false, + message: '缺少文件路径参数' + }); + } + + const normalizedPath = path.posix.normalize(filePath); + if (normalizedPath.includes('..') || filePath.includes('\x00')) { + return res.status(400).json({ + success: false, + message: '文件路径非法' + }); + } + + try { + const policyState = enforceDownloadTrafficPolicy(req.user.id, 'download_check'); + const latestUser = policyState?.user || UserDB.findById(req.user.id); + if (!latestUser) { + return res.status(401).json({ + success: false, + message: '用户不存在' + }); + } + const trafficState = getDownloadTrafficState(latestUser); + + const { StorageInterface } = require('./storage'); + const storageInterface = new StorageInterface(req.user); + storage = await storageInterface.connect(); + + const fileStats = await storage.stat(normalizedPath); + const fileSize = Math.max(0, Number(fileStats?.size) || 0); + const fileName = normalizedPath.split('/').pop() || 'download.bin'; + + if (!trafficState.isUnlimited && fileSize > trafficState.remaining) { + return res.status(403).json({ + success: false, + message: `下载流量不足:文件 ${formatFileSize(fileSize)},剩余 ${formatFileSize(trafficState.remaining)}` + }); + } + + res.json({ + success: true, + fileName, + fileSize + }); + } catch (error) { + console.error('下载预检失败:', error); + res.status(500).json({ + success: false, + message: getSafeErrorMessage(error, '下载检查失败,请稍后重试', '下载预检') + }); + } finally { + if (storage) await storage.end(); + } +}); // 下载文件 app.get('/api/files/download', authMiddleware, async (req, res) => { diff --git a/frontend/app.js b/frontend/app.js index 7a2fc6f..d29383f 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1562,7 +1562,7 @@ handleDragLeave(e) { } // 其他场景走后端下载接口(支持下载流量计量/权限控制) - this.downloadFromLocal(filePath); + await this.downloadFromLocal(filePath); }, async downloadFromOSS(filePath) { @@ -1595,15 +1595,35 @@ handleDragLeave(e) { } }, - // 本地存储下载 - downloadFromLocal(filePath) { - const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`; - const link = document.createElement('a'); - link.href = url; - link.setAttribute('download', ''); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + // 本地存储下载(先预检,避免浏览器下载 JSON 错误文件) + async downloadFromLocal(filePath) { + try { + const checkResp = await axios.get(`${this.apiBase}/api/files/download-check`, { + params: { path: filePath } + }); + + if (!checkResp.data?.success) { + this.showToast('error', '下载失败', checkResp.data?.message || '下载失败,请稍后重试'); + return false; + } + + const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`; + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', ''); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + return true; + } catch (error) { + console.error('本地下载预检失败:', error); + const message = error.response?.data?.message || '下载失败,请稍后重试'; + this.showToast('error', '下载失败', message); + if (error.response?.status === 401) { + this.logout(); + } + return false; + } }, // ===== 文件操作 =====