From af5d0f43a5bf041131782103726bee0b2f315507 Mon Sep 17 00:00:00 2001 From: yangxiangyuan Date: Sun, 2 Aug 2026 21:00:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(private=5Fclipboard):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=A7=81=E6=9C=89=E5=89=AA=E8=B4=B4=E6=9D=BF=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E6=8E=A5=E5=8F=A3=E5=B9=B6=E8=B0=83=E6=95=B4?= =?UTF-8?q?url=E8=BF=94=E5=9B=9E=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不再直接暴露本地文件路径作为文件url,改用api接口代理访问;新增对应路由处理文件的预览和下载请求,完善响应头配置。 --- src/server/private_clipboard.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/server/private_clipboard.js b/src/server/private_clipboard.js index 099c0bb..6a6a34f 100644 --- a/src/server/private_clipboard.js +++ b/src/server/private_clipboard.js @@ -101,8 +101,7 @@ const saveFile = (raw, name, mime) => { } const formatRow = (row) => { - const filePath = row.file_path ? String(row.file_path) : '' - const url = filePath ? '/' + filePath.replace(/^\/+/, '') : '' + const url = row && row.id && row.file_path ? `/api/private_clipboard/file/${row.id}` : '' return { id: row.id, type: row.type, @@ -402,6 +401,30 @@ const createRouter = () => { // ===== 以下业务路由需要登录 ===== router.use(mustAuth) + router.get('/file/:id', (req, res) => { + try { + const id = Number.parseInt(String(req.params.id || ''), 10) + if (!id) return res.status(400).json({ ok: false, error: 'invalid_id' }) + const row = getDb().prepare('SELECT id, type, file_name, file_mime, file_path FROM clipboard_items WHERE id = ?').get(id) + if (!row || !row.file_path) return res.status(404).json({ ok: false, error: 'file_not_found' }) + const rel = String(row.file_path || '').replace(/^\/+/, '') + const abs = path.join(process.cwd(), rel) + if (!fs.existsSync(abs)) return res.status(404).json({ ok: false, error: 'file_not_found' }) + const mime = String(row.file_mime || '').trim() || 'application/octet-stream' + res.setHeader('Content-Type', mime) + res.setHeader('Cache-Control', 'private, no-store, max-age=0') + const safeName = normalizeName(row.file_name || 'file') + const isImage = String(row.type || '').toLowerCase() === 'image' || mime.toLowerCase().startsWith('image/') + const dispositionType = isImage ? 'inline' : 'attachment' + const fallbackName = safeName.replace(/[^\x20-\x7E]/g, '_') + const utf8Name = encodeURIComponent(safeName) + res.setHeader('Content-Disposition', `${dispositionType}; filename="${fallbackName}"; filename*=UTF-8''${utf8Name}`) + return res.sendFile(abs) + } catch (e) { + return res.status(500).json({ ok: false, error: String(e.message || e) }) + } + }) + router.get('/list', (req, res) => { try { const pageSizeRaw = String((req.query && req.query.page_size) || '50').trim().toLowerCase()