From e5c932a3515a61f217e7ae55c237afe8330b8092 Mon Sep 17 00:00:00 2001 From: WanWanYun Date: Sat, 15 Nov 2025 23:46:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E6=96=87=E4=BB=B6=E5=A4=B9=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新功能概述: - 支持在本地存储模式下创建文件夹 - 支持删除文件夹(递归删除) - 支持重命名文件夹(已有功能,天然支持) - 文件夹配额计算正确 后端改动: 1. 新增创建文件夹API (backend/server.js) - POST /api/files/mkdir - 参数: path(当前路径), folderName(文件夹名称) - 安全检查: 禁止特殊字符(/ \ .. :),防止路径遍历攻击 - 仅限本地存储使用 - 创建前检查文件夹是否已存在 2. 改进删除功能 (backend/storage.js) - LocalStorageClient.delete() 现在支持删除文件夹 - 使用 fs.rmSync(path, { recursive: true }) 递归删除 - 新增 calculateFolderSize() 方法计算文件夹总大小 - 删除文件夹时正确更新配额使用情况 前端改动: 1. 新建文件夹按钮 (frontend/app.html) - 在"上传文件"按钮旁新增"新建文件夹"按钮 - 仅本地存储模式显示 2. 新建文件夹弹窗 (frontend/app.html) - 简洁的创建表单 - 支持回车键快速创建 - 使用优化的弹窗关闭逻辑(防止拖动选择文本时误关闭) 3. 前端API调用 (frontend/app.js) - 新增 createFolderForm 状态 - 新增 createFolder() 方法 - 前端参数验证 - 创建成功后自动刷新文件列表 4. 右键菜单优化 (frontend/app.html) - 文件夹不显示"下载"按钮(文件夹暂不支持打包下载) - 文件夹不显示"分享"按钮(分享单个文件夹暂不支持) - 文件夹支持"重命名"和"删除"操作 安全性: - 文件夹名称严格验证,禁止包含 / \ .. : 等特殊字符 - 路径安全检查,防止目录遍历攻击 - 仅限本地存储模式使用(SFTP存储使用上传工具管理) 配额管理: - 空文件夹不占用配额 - 删除文件夹时正确释放配额(计算所有子文件大小) - 删除非空文件夹会递归删除所有内容 使用方式: 1. 登录后切换到本地存储模式 2. 点击"新建文件夹"按钮 3. 输入文件夹名称,点击创建 4. 双击文件夹进入,支持多级目录 5. 右键文件夹可重命名或删除 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/server.js | 69 +++++++++++++++++++++++++++++++++++++++++++++- backend/storage.js | 47 ++++++++++++++++++++++++++++--- frontend/app.html | 29 +++++++++++++++++-- frontend/app.js | 6 ++++ 4 files changed, 144 insertions(+), 7 deletions(-) diff --git a/backend/server.js b/backend/server.js index 9954903..f90e308 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1066,6 +1066,73 @@ app.post('/api/files/rename', authMiddleware, async (req, res) => { } }); +// 创建文件夹 +app.post('/api/files/mkdir', authMiddleware, async (req, res) => { + const { path, folderName } = req.body; + let storage; + + // 参数验证 + if (!folderName || folderName.trim() === '') { + return res.status(400).json({ + success: false, + message: '文件夹名称不能为空' + }); + } + + // 文件名安全检查 - 防止路径遍历攻击 + if (folderName.includes('/') || folderName.includes('\\') || folderName.includes('..') || folderName.includes(':')) { + return res.status(400).json({ + success: false, + message: '文件夹名称不能包含特殊字符 (/ \\ .. :)' + }); + } + + // 只允许本地存储创建文件夹 + if (req.user.current_storage_type !== 'local') { + return res.status(400).json({ + success: false, + message: '只有本地存储支持创建文件夹' + }); + } + + try { + const { StorageInterface } = require('./storage'); + const storageInterface = new StorageInterface(req.user); + storage = await storageInterface.connect(); + + // 构造文件夹路径 + const basePath = path || '/'; + const folderPath = basePath === '/' ? `/${folderName}` : `${basePath}/${folderName}`; + const fullPath = storage.getFullPath(folderPath); + + // 检查是否已存在 + if (fs.existsSync(fullPath)) { + return res.status(400).json({ + success: false, + message: '文件夹已存在' + }); + } + + // 创建文件夹 (不使用recursive,只创建当前层级) + fs.mkdirSync(fullPath, { mode: 0o755 }); + + console.log(`[创建文件夹成功] 用户${req.user.id}: ${folderPath}`); + + res.json({ + success: true, + message: '文件夹创建成功' + }); + } catch (error) { + console.error('[创建文件夹失败]', error); + res.status(500).json({ + success: false, + message: '创建文件夹失败: ' + error.message + }); + } finally { + if (storage) await storage.end(); + } +}); + // 删除文件 app.post('/api/files/delete', authMiddleware, async (req, res) => { const { fileName, path } = req.body; @@ -1090,7 +1157,7 @@ app.post('/api/files/delete', authMiddleware, async (req, res) => { res.json({ success: true, - message: '文件删除成功' + message: '删除成功' }); } catch (error) { console.error('删除文件失败:', error); diff --git a/backend/storage.js b/backend/storage.js index 4257508..493ef6f 100644 --- a/backend/storage.js +++ b/backend/storage.js @@ -137,16 +137,55 @@ class LocalStorageClient { } /** - * 删除文件 + * 删除文件或文件夹 */ async delete(filePath) { const fullPath = this.getFullPath(filePath); const stats = fs.statSync(fullPath); - fs.unlinkSync(fullPath); + if (stats.isDirectory()) { + // 删除文件夹 - 递归删除 + // 先计算文件夹内所有文件的总大小 + const folderSize = this.calculateFolderSize(fullPath); - // 更新已使用空间 - this.updateUsedSpace(-stats.size); + // 删除文件夹及其内容 + fs.rmSync(fullPath, { recursive: true, force: true }); + + // 更新已使用空间 + if (folderSize > 0) { + this.updateUsedSpace(-folderSize); + } + } else { + // 删除文件 + fs.unlinkSync(fullPath); + + // 更新已使用空间 + this.updateUsedSpace(-stats.size); + } + } + + /** + * 计算文件夹大小 + */ + calculateFolderSize(folderPath) { + let totalSize = 0; + + const items = fs.readdirSync(folderPath, { withFileTypes: true }); + + for (const item of items) { + const itemPath = path.join(folderPath, item.name); + + if (item.isDirectory()) { + // 递归计算子文件夹 + totalSize += this.calculateFolderSize(itemPath); + } else { + // 累加文件大小 + const stats = fs.statSync(itemPath); + totalSize += stats.size; + } + } + + return totalSize; } /** diff --git a/frontend/app.html b/frontend/app.html index 6de58f6..21cb2f9 100644 --- a/frontend/app.html +++ b/frontend/app.html @@ -669,6 +669,9 @@ + + + + + +