|
@@ -11,9 +11,16 @@ package logic
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
"context"
|
|
"context"
|
|
|
|
|
+ "encoding/json"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "io"
|
|
|
|
|
+ "net"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "net/url"
|
|
|
"os"
|
|
"os"
|
|
|
- "path/filepath"
|
|
|
|
|
|
|
+ "path"
|
|
|
"strings"
|
|
"strings"
|
|
|
|
|
+ "time"
|
|
|
|
|
|
|
|
"udeServer/internal/apierr"
|
|
"udeServer/internal/apierr"
|
|
|
"udeServer/internal/svc"
|
|
"udeServer/internal/svc"
|
|
@@ -24,13 +31,13 @@ import (
|
|
|
|
|
|
|
|
// 根层直接屏蔽的系统/虚拟目录(仅在这些目录位于 "/" 下时过滤)
|
|
// 根层直接屏蔽的系统/虚拟目录(仅在这些目录位于 "/" 下时过滤)
|
|
|
var dirBlacklist = map[string]bool{
|
|
var dirBlacklist = map[string]bool{
|
|
|
- "proc": true,
|
|
|
|
|
- "sys": true,
|
|
|
|
|
- "dev": true,
|
|
|
|
|
- "run": true,
|
|
|
|
|
- "boot": true,
|
|
|
|
|
- "lost+found": true,
|
|
|
|
|
- "snap": true,
|
|
|
|
|
|
|
+ "proc": true,
|
|
|
|
|
+ "sys": true,
|
|
|
|
|
+ "dev": true,
|
|
|
|
|
+ "run": true,
|
|
|
|
|
+ "boot": true,
|
|
|
|
|
+ "lost+found": true,
|
|
|
|
|
+ "snap": true,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
type ListDirChildrenLogic struct {
|
|
type ListDirChildrenLogic struct {
|
|
@@ -47,6 +54,19 @@ func NewListDirChildrenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *L
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// --- 远程 /list 响应结构 -------------------------------------------------
|
|
|
|
|
+type remoteListResp struct {
|
|
|
|
|
+ Path string `json:"path"`
|
|
|
|
|
+ Total int `json:"total"`
|
|
|
|
|
+ Entries []remoteEntry `json:"entries"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type remoteEntry struct {
|
|
|
|
|
+ Name string `json:"name"`
|
|
|
|
|
+ Dir bool `json:"dir"`
|
|
|
|
|
+ Size int64 `json:"size"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*types.DirChildrenResp, error) {
|
|
func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*types.DirChildrenResp, error) {
|
|
|
// 1. 归一化: 空 → 根目录; 必须以 / 开头; Clean 防 ../ 穿越
|
|
// 1. 归一化: 空 → 根目录; 必须以 / 开头; Clean 防 ../ 穿越
|
|
|
p := strings.TrimSpace(req.Path)
|
|
p := strings.TrimSpace(req.Path)
|
|
@@ -56,42 +76,120 @@ func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*
|
|
|
if !strings.HasPrefix(p, "/") {
|
|
if !strings.HasPrefix(p, "/") {
|
|
|
return nil, apierr.BadRequest("path 必须是绝对路径(以 / 开头)")
|
|
return nil, apierr.BadRequest("path 必须是绝对路径(以 / 开头)")
|
|
|
}
|
|
}
|
|
|
- dir := filepath.Clean(p)
|
|
|
|
|
|
|
+ // 目标机是 Linux,统一用 path.Clean,避免依赖本机 OS 分隔符
|
|
|
|
|
+ dir := path.Clean(p)
|
|
|
|
|
|
|
|
- // 2. 读取该层目录(只读一层,天然懒加载)
|
|
|
|
|
- entries, err := os.ReadDir(dir)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- // 权限不足等场景: 记日志 + 返回空列表,不中断前端树的展开
|
|
|
|
|
- l.Errorf("read dir %s failed: %v", dir, err)
|
|
|
|
|
- return &types.DirChildrenResp{Nodes: []types.LocalDirNode{}}, nil
|
|
|
|
|
|
|
+ if req.Ip == "" {
|
|
|
|
|
+ return nil, apierr.BadRequest("ip 不能为空")
|
|
|
|
|
+ }
|
|
|
|
|
+ if req.Port == "" {
|
|
|
|
|
+ return nil, apierr.BadRequest("port 不能为空")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. 挂载根目录前缀(该目录树下的挂载点不参与选择)
|
|
|
|
|
- baseDir := strings.TrimSuffix(l.svcCtx.Config.Mount.BaseDir, "/")
|
|
|
|
|
|
|
+ // 2. 调用远程 /list 接口(只读一层,天然懒加载)
|
|
|
|
|
+ remote, err := l.fetchRemoteList(req.Ip, req.Port, dir)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- nodes := make([]types.LocalDirNode, 0, len(entries))
|
|
|
|
|
- for _, e := range entries {
|
|
|
|
|
- if !e.IsDir() {
|
|
|
|
|
- continue // 只要目录,文件一律跳过
|
|
|
|
|
|
|
+ // 3. 转换成前端节点结构(响应结构与原来保持一致)
|
|
|
|
|
+ nodes := make([]types.LocalDirNode, 0, len(remote.Entries))
|
|
|
|
|
+ for _, e := range remote.Entries {
|
|
|
|
|
+ if !e.Dir {
|
|
|
|
|
+ continue // 只要目录;文件/符号链接(dir:false)一律跳过
|
|
|
}
|
|
}
|
|
|
- name := e.Name()
|
|
|
|
|
|
|
+ name := e.Name
|
|
|
if dir == "/" && dirBlacklist[name] {
|
|
if dir == "/" && dirBlacklist[name] {
|
|
|
continue // 根层黑名单系统目录
|
|
continue // 根层黑名单系统目录
|
|
|
}
|
|
}
|
|
|
- full := filepath.ToSlash(filepath.Join(dir, name))
|
|
|
|
|
- if baseDir != "" && (full == baseDir || strings.HasPrefix(full, baseDir+"/")) {
|
|
|
|
|
- continue // ude 自己的挂载根目录不展示
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ full := path.Join(dir, name)
|
|
|
nodes = append(nodes, types.LocalDirNode{
|
|
nodes = append(nodes, types.LocalDirNode{
|
|
|
- Name: name,
|
|
|
|
|
- Path: full,
|
|
|
|
|
- IsDir: true,
|
|
|
|
|
- HasChildren: hasSubDir(full),
|
|
|
|
|
|
|
+ Name: name,
|
|
|
|
|
+ Path: full,
|
|
|
|
|
+ IsDir: true,
|
|
|
|
|
+ // 远程 /list 不返回"是否有子目录",保守给 true;
|
|
|
|
|
+ // 前端真正展开时会再发一次 /list,行为上仍是懒加载
|
|
|
|
|
+ HasChildren: true,
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
return &types.DirChildrenResp{Nodes: nodes}, nil
|
|
return &types.DirChildrenResp{Nodes: nodes}, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// --- 调用远程 /list ------------------------------------------------------
|
|
|
|
|
+func (l *ListDirChildrenLogic) fetchRemoteList(ip, port, dir string) (*remoteListResp, error) {
|
|
|
|
|
+ u := &url.URL{
|
|
|
|
|
+ Scheme: "http",
|
|
|
|
|
+ Host: net.JoinHostPort(ip, port),
|
|
|
|
|
+ Path: "/list",
|
|
|
|
|
+ }
|
|
|
|
|
+ q := url.Values{}
|
|
|
|
|
+ q.Set("path", dir)
|
|
|
|
|
+ u.RawQuery = q.Encode()
|
|
|
|
|
+
|
|
|
|
|
+ ctx, cancel := context.WithTimeout(l.ctx, 10*time.Second)
|
|
|
|
|
+ defer cancel()
|
|
|
|
|
+
|
|
|
|
|
+ httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, apierr.BadRequest(fmt.Sprintf("构造远程请求失败: %v", err))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ httpResp, err := http.DefaultClient.Do(httpReq)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ l.Errorf("call remote /list %s failed: %v", u.String(), err)
|
|
|
|
|
+ return nil, apierr.BadRequest(fmt.Sprintf("调用远程接口失败: %v", err))
|
|
|
|
|
+ }
|
|
|
|
|
+ defer httpResp.Body.Close()
|
|
|
|
|
+
|
|
|
|
|
+ // 保护性限制:一个目录条目正常远小于 8MB
|
|
|
|
|
+ body, readErr := io.ReadAll(io.LimitReader(httpResp.Body, 8<<20))
|
|
|
|
|
+ if readErr != nil {
|
|
|
|
|
+ return nil, apierr.BadRequest(fmt.Sprintf("读取远程响应失败: %v", readErr))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch httpResp.StatusCode {
|
|
|
|
|
+ case http.StatusOK:
|
|
|
|
|
+ // ok,继续解析
|
|
|
|
|
+ case http.StatusBadRequest:
|
|
|
|
|
+ // 远程 400 自带中文说明,原样透传给前端
|
|
|
|
|
+ return nil, apierr.BadRequest(extractRemoteMessage(body))
|
|
|
|
|
+ case http.StatusMethodNotAllowed:
|
|
|
|
|
+ return nil, apierr.BadRequest("远程服务拒绝了 GET 请求")
|
|
|
|
|
+ default:
|
|
|
|
|
+ return nil, apierr.BadRequest(fmt.Sprintf(
|
|
|
|
|
+ "远程接口异常(%d): %s", httpResp.StatusCode, extractRemoteMessage(body)))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var out remoteListResp
|
|
|
|
|
+ if err := json.Unmarshal(body, &out); err != nil {
|
|
|
|
|
+ return nil, apierr.BadRequest(fmt.Sprintf("解析远程响应失败: %v", err))
|
|
|
|
|
+ }
|
|
|
|
|
+ return &out, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 从远程错误响应中提取中文说明(兼容 message/msg/error 字段)
|
|
|
|
|
+func extractRemoteMessage(body []byte) string {
|
|
|
|
|
+ if len(body) == 0 {
|
|
|
|
|
+ return "远程接口返回空响应"
|
|
|
|
|
+ }
|
|
|
|
|
+ var wrap struct {
|
|
|
|
|
+ Message string `json:"message"`
|
|
|
|
|
+ Msg string `json:"msg"`
|
|
|
|
|
+ Error string `json:"error"`
|
|
|
|
|
+ }
|
|
|
|
|
+ if json.Unmarshal(body, &wrap) == nil {
|
|
|
|
|
+ switch {
|
|
|
|
|
+ case wrap.Message != "":
|
|
|
|
|
+ return wrap.Message
|
|
|
|
|
+ case wrap.Msg != "":
|
|
|
|
|
+ return wrap.Msg
|
|
|
|
|
+ case wrap.Error != "":
|
|
|
|
|
+ return wrap.Error
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return string(body)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// hasSubDir 判断目录下是否还有子目录(只读一层、找到一个即返回,开销可控)
|
|
// hasSubDir 判断目录下是否还有子目录(只读一层、找到一个即返回,开销可控)
|
|
|
func hasSubDir(dir string) bool {
|
|
func hasSubDir(dir string) bool {
|
|
|
subs, err := os.ReadDir(dir)
|
|
subs, err := os.ReadDir(dir)
|
|
@@ -104,4 +202,4 @@ func hasSubDir(dir string) bool {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return false
|
|
return false
|
|
|
-}
|
|
|
|
|
|
|
+}
|