Переглянути джерело

feat:1.改为从外挂程序拉取远程服务器的目录

wangkun 1 тиждень тому
батько
коміт
5e7236b31b
2 змінених файлів з 131 додано та 31 видалено
  1. 129 31
      internal/logic/listdirchildrenlogic.go
  2. 2 0
      internal/types/types.go

+ 129 - 31
internal/logic/listdirchildrenlogic.go

@@ -11,9 +11,16 @@ package logic
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"net/url"
 	"os"
-	"path/filepath"
+	"path"
 	"strings"
+	"time"
 
 	"udeServer/internal/apierr"
 	"udeServer/internal/svc"
@@ -24,13 +31,13 @@ import (
 
 // 根层直接屏蔽的系统/虚拟目录(仅在这些目录位于 "/" 下时过滤)
 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 {
@@ -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) {
 	// 1. 归一化: 空 → 根目录; 必须以 / 开头; Clean 防 ../ 穿越
 	p := strings.TrimSpace(req.Path)
@@ -56,42 +76,120 @@ func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*
 	if !strings.HasPrefix(p, "/") {
 		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] {
 			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{
-			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
 }
 
+// --- 调用远程 /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 判断目录下是否还有子目录(只读一层、找到一个即返回,开销可控)
 func hasSubDir(dir string) bool {
 	subs, err := os.ReadDir(dir)
@@ -104,4 +202,4 @@ func hasSubDir(dir string) bool {
 		}
 	}
 	return false
-}
+}

+ 2 - 0
internal/types/types.go

@@ -95,6 +95,8 @@ type Response struct {
 
 // 本地目录懒加载(udeServer 所在 Linux 主机文件系统,只列目录不列文件)
 type ListDirChildrenReq struct {
+	Ip   string `form:"ip"` // 目标 Linux 主机 IP
+	Port string `form:"port"`
 	Path string `form:"path"` // 空或 "/" 表示根目录
 }