diff --git a/plugins/file-manager/manifest.json b/plugins/file-manager/manifest.json index 1a8ecfd..7b4cae6 100644 --- a/plugins/file-manager/manifest.json +++ b/plugins/file-manager/manifest.json @@ -1,9 +1,19 @@ { "name": "file-manager", - "version": "1.0.0", - "description": "文件管理器插件,提供远程文件浏览和管理功能", + "version": "2.0.0", + "description": "Web 文件管理器,提供远程文件浏览、上传、下载和管理功能", "author": "GoTunnel Official", "run_at": "client", "type": "app", - "icon": "icons/file-manager.svg" -} + "icon": "icons/file-manager.svg", + "homepage": "https://github.com/flik/GoTunnel-Plugins", + "config_schema": [ + { + "key": "root_path", + "label": "根目录", + "type": "string", + "default": ".", + "description": "文件管理的根目录路径,所有操作将限制在此目录内" + } + ] +} \ No newline at end of file diff --git a/plugins/file-manager/plugin.js b/plugins/file-manager/plugin.js index 1400913..e37c1ac 100644 --- a/plugins/file-manager/plugin.js +++ b/plugins/file-manager/plugin.js @@ -1,64 +1,725 @@ -// GoTunnel File Manager Plugin +// GoTunnel File Manager Plugin v2.0.0 +// 提供完整的 Web 文件管理界面 + function metadata() { return { name: "file-manager", - version: "1.0.0", - description: "文件管理器插件", + version: "2.0.0", + description: "Web 文件管理器,提供远程文件浏览、上传、下载和管理功能", author: "GoTunnel Official", type: "app", run_at: "client" }; } +var rootPath = "."; + function start() { - log("File Manager plugin started"); + rootPath = config("root_path") || "."; + log("File Manager started, root: " + rootPath); } function stop() { - log("File Manager plugin stopped"); + log("File Manager stopped"); } function handleConn(conn) { http.serve(conn, handleRequest); } +// ============================================================================ +// 请求路由 +// ============================================================================ + function handleRequest(req) { var path = req.path; var method = req.method; - if (path === "/api/list") { - return handleList(req); + // 静态页面 + if (path === "/" || path === "/index.html") { + return { status: 200, contentType: "text/html", body: getIndexHTML() }; + } + + // API 路由 + if (path === "/api/list" && method === "GET") { + return apiList(req); + } + if (path === "/api/stat" && method === "GET") { + return apiStat(req); } if (path === "/api/read" && method === "POST") { - return handleRead(req); + return apiRead(req); + } + if (path === "/api/write" && method === "POST") { + return apiWrite(req); + } + if (path === "/api/mkdir" && method === "POST") { + return apiMkdir(req); + } + if (path === "/api/delete" && method === "POST") { + return apiDelete(req); + } + if (path === "/api/rename" && method === "POST") { + return apiRename(req); + } + if (path === "/api/download" && method === "GET") { + return apiDownload(req); } - return { status: 404, body: '{"error":"not found"}' }; + return jsonError(404, "Not Found"); } -function handleList(req) { - var dir = config("root_path") || "."; - var result = fs.readDir(dir); +// ============================================================================ +// API 处理函数 +// ============================================================================ +function apiList(req) { + var dir = getQueryParam(req.path, "path") || "/"; + var fullPath = resolvePath(dir); + + if (!fullPath) { + return jsonError(403, "Access denied"); + } + + var result = fs.readDir(fullPath); if (result.error) { - return { status: 500, body: http.json({ error: result.error }) }; + return jsonError(500, result.error); } - return { status: 200, body: http.json({ entries: result.entries }) }; + var entries = []; + for (var i = 0; i < result.entries.length; i++) { + var e = result.entries[i]; + entries.push({ + name: e.name, + isDir: e.isDir, + size: e.size || 0 + }); + } + + // 排序:文件夹在前,然后按名称 + entries.sort(function (a, b) { + if (a.isDir !== b.isDir) return a.isDir ? -1 : 1; + return a.name.localeCompare(b.name); + }); + + return jsonSuccess({ path: dir, entries: entries }); } -function handleRead(req) { - var body = JSON.parse(req.body || "{}"); +function apiStat(req) { + var path = getQueryParam(req.path, "path") || "/"; + var fullPath = resolvePath(path); + + if (!fullPath) { + return jsonError(403, "Access denied"); + } + + var result = fs.stat(fullPath); + if (result.error) { + return jsonError(404, result.error); + } + + return jsonSuccess({ + name: result.name, + size: result.size, + isDir: result.isDir, + modTime: result.modTime + }); +} + +function apiRead(req) { + var body = parseBody(req.body); var path = body.path; if (!path) { - return { status: 400, body: '{"error":"path required"}' }; + return jsonError(400, "path required"); } - var result = fs.readFile(path); + var fullPath = resolvePath(path); + if (!fullPath) { + return jsonError(403, "Access denied"); + } + + var result = fs.readFile(fullPath); if (result.error) { - return { status: 500, body: http.json({ error: result.error }) }; + return jsonError(500, result.error); } - return { status: 200, body: http.json({ content: result.data }) }; + return jsonSuccess({ content: result.data }); +} + +function apiWrite(req) { + var body = parseBody(req.body); + var path = body.path; + var content = body.content; + + if (!path) { + return jsonError(400, "path required"); + } + + var fullPath = resolvePath(path); + if (!fullPath) { + return jsonError(403, "Access denied"); + } + + var result = fs.writeFile(fullPath, content || ""); + if (!result.ok) { + return jsonError(500, result.error); + } + + return jsonSuccess({ message: "File saved" }); +} + +function apiMkdir(req) { + var body = parseBody(req.body); + var path = body.path; + + if (!path) { + return jsonError(400, "path required"); + } + + var fullPath = resolvePath(path); + if (!fullPath) { + return jsonError(403, "Access denied"); + } + + var result = fs.mkdir(fullPath); + if (!result.ok) { + return jsonError(500, result.error); + } + + return jsonSuccess({ message: "Directory created" }); +} + +function apiDelete(req) { + var body = parseBody(req.body); + var path = body.path; + + if (!path) { + return jsonError(400, "path required"); + } + + var fullPath = resolvePath(path); + if (!fullPath) { + return jsonError(403, "Access denied"); + } + + var result = fs.remove(fullPath); + if (!result.ok) { + return jsonError(500, result.error); + } + + return jsonSuccess({ message: "Deleted" }); +} + +function apiRename(req) { + var body = parseBody(req.body); + var oldPath = body.path; + var newPath = body.newPath; + + if (!oldPath || !newPath) { + return jsonError(400, "path and newPath required"); + } + + var fullOldPath = resolvePath(oldPath); + var fullNewPath = resolvePath(newPath); + + if (!fullOldPath || !fullNewPath) { + return jsonError(403, "Access denied"); + } + + // 读取旧文件 + var stat = fs.stat(fullOldPath); + if (stat.error) { + return jsonError(404, stat.error); + } + + if (stat.isDir) { + return jsonError(400, "Cannot rename directories"); + } + + var content = fs.readFile(fullOldPath); + if (content.error) { + return jsonError(500, content.error); + } + + // 写入新位置 + var writeResult = fs.writeFile(fullNewPath, content.data); + if (!writeResult.ok) { + return jsonError(500, writeResult.error); + } + + // 删除旧文件 + fs.remove(fullOldPath); + + return jsonSuccess({ message: "Renamed" }); +} + +function apiDownload(req) { + var path = getQueryParam(req.path, "path"); + + if (!path) { + return jsonError(400, "path required"); + } + + var fullPath = resolvePath(path); + if (!fullPath) { + return jsonError(403, "Access denied"); + } + + var result = fs.readFile(fullPath); + if (result.error) { + return jsonError(404, result.error); + } + + var filename = path.split("/").pop() || "download"; + + return { + status: 200, + contentType: "application/octet-stream", + body: result.data + }; +} + +// ============================================================================ +// 工具函数 +// ============================================================================ + +function resolvePath(path) { + // 规范化路径 + path = path || "/"; + + // 移除开头的斜杠 + while (path.charAt(0) === "/") { + path = path.substring(1); + } + + // 检查路径穿越攻击 + if (path.indexOf("..") !== -1) { + return null; + } + + // 拼接根路径 + if (path === "") { + return rootPath; + } + + return rootPath + "/" + path; +} + +function getQueryParam(path, key) { + var idx = path.indexOf("?"); + if (idx === -1) return ""; + + var query = path.substring(idx + 1); + var pairs = query.split("&"); + + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split("="); + if (pair[0] === key) { + return decodeURIComponent(pair[1] || ""); + } + } + return ""; +} + +function parseBody(body) { + try { + return JSON.parse(body || "{}"); + } catch (e) { + return {}; + } +} + +function jsonSuccess(data) { + return { + status: 200, + contentType: "application/json", + body: http.json({ success: true, data: data }) + }; +} + +function jsonError(status, message) { + return { + status: status, + contentType: "application/json", + body: http.json({ success: false, error: message }) + }; +} + +// ============================================================================ +// Web UI - HTML/CSS/JavaScript +// ============================================================================ + +function getIndexHTML() { + return '\n\ +\n\ +\n\ + \n\ + \n\ + File Manager\n\ + \n\ +\n\ +\n\ +
\n\ +
\n\ +

File Manager

\n\ + \n\ +
\n\ +
\n\ + \n\ + \n\ + \n\ +
\n\ +
\n\ +
加载中...
\n\ +
\n\ +
\n\ +
\n\ + \n\ +\n\ +'; } diff --git a/store.json b/store.json index c485f7f..9e7daf8 100644 --- a/store.json +++ b/store.json @@ -1,13 +1,13 @@ [ -{ - "name": "file-manager", - "version": "1.0.0", - "description": "文件管理器插件,提供远程文件浏览和管理功能", - "author": "GoTunnel Official", - "run_at": "client", - "type": "app", - "icon": "icons/file-manager.svg", - "download_url": "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/plugins/file-manager/plugin.js", - "signature_url": "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/plugins/file-manager/plugin.js.sig" -} -] + { + "name": "file-manager", + "version": "2.0.0", + "description": "Web 文件管理器,提供远程文件浏览、上传、下载和管理功能", + "author": "GoTunnel Official", + "run_at": "client", + "type": "app", + "icon": "icons/file-manager.svg", + "download_url": "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/plugins/file-manager/plugin.js", + "signature_url": "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/plugins/file-manager/plugin.js.sig" + } +] \ No newline at end of file