listdirchildrenlogic.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. // udeServer: 本地目录懒加载 —— 浏览 udeServer 所在 Linux 主机的文件系统。
  2. // 只列目录、不列文件;从根目录 "/" 开始,前端每展开一个节点请求一次该层子目录。
  3. // 安全边界:
  4. // 1. 路径归一化: 必须是绝对路径,filepath.Clean 防穿越
  5. // 2. 黑名单: /proc /sys /dev /run /boot 等系统目录不展示
  6. // 3. 挂载根目录(Mount.BaseDir, 如 /var/ude/mnt)不展示,避免把已挂载点当普通目录选
  7. // 4. 符号链接: DirEntry.IsDir() 对 symlink 返回 false,天然跳过,不跟随
  8. // 5. 权限不足目录: 返回空列表而非报错,树展开不中断
  9. package logic
  10. import (
  11. "context"
  12. "encoding/json"
  13. "io"
  14. "net"
  15. "net/http"
  16. "net/url"
  17. "os"
  18. "path"
  19. "strings"
  20. "time"
  21. "udeServer/internal/apierr"
  22. "udeServer/internal/svc"
  23. "udeServer/internal/types"
  24. "github.com/zeromicro/go-zero/core/logx"
  25. )
  26. // 根层直接屏蔽的系统/虚拟目录(仅在这些目录位于 "/" 下时过滤)
  27. var dirBlacklist = map[string]bool{
  28. "proc": true,
  29. "sys": true,
  30. "dev": true,
  31. "run": true,
  32. "boot": true,
  33. "lost+found": true,
  34. "snap": true,
  35. }
  36. type ListDirChildrenLogic struct {
  37. logx.Logger
  38. ctx context.Context
  39. svcCtx *svc.ServiceContext
  40. }
  41. func NewListDirChildrenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListDirChildrenLogic {
  42. return &ListDirChildrenLogic{
  43. Logger: logx.WithContext(ctx),
  44. ctx: ctx,
  45. svcCtx: svcCtx,
  46. }
  47. }
  48. // --- 远程 /list 响应结构 -------------------------------------------------
  49. type remoteListResp struct {
  50. Path string `json:"path"`
  51. Total int `json:"total"`
  52. Entries []remoteEntry `json:"entries"`
  53. }
  54. type remoteEntry struct {
  55. Name string `json:"name"`
  56. Dir bool `json:"dir"`
  57. Size int64 `json:"size"`
  58. }
  59. func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*types.DirChildrenResp, error) {
  60. // 1. 归一化: 空 → 根目录; 必须以 / 开头; Clean 防 ../ 穿越
  61. p := strings.TrimSpace(req.Path)
  62. if p == "" {
  63. p = "/"
  64. }
  65. if !strings.HasPrefix(p, "/") {
  66. return nil, apierr.BadRequest("path 必须是绝对路径(以 / 开头)")
  67. }
  68. // 目标机是 Linux,统一用 path.Clean,避免依赖本机 OS 分隔符
  69. dir := path.Clean(p)
  70. if req.Ip == "" {
  71. return nil, apierr.BadRequest("ip 不能为空")
  72. }
  73. if req.Port == "" {
  74. return nil, apierr.BadRequest("port 不能为空")
  75. }
  76. // 2. 调用远程 /list 接口(只读一层,天然懒加载)
  77. remote, err := l.fetchRemoteList(req.Ip, req.Port, dir)
  78. if err != nil {
  79. return nil, err
  80. }
  81. // 3. 转换成前端节点结构(响应结构与原来保持一致)
  82. nodes := make([]types.LocalDirNode, 0, len(remote.Entries))
  83. for _, e := range remote.Entries {
  84. if !e.Dir {
  85. continue // 只要目录;文件/符号链接(dir:false)一律跳过
  86. }
  87. name := e.Name
  88. if dir == "/" && dirBlacklist[name] {
  89. continue // 根层黑名单系统目录
  90. }
  91. full := path.Join(dir, name)
  92. nodes = append(nodes, types.LocalDirNode{
  93. Name: name,
  94. Path: full,
  95. IsDir: true,
  96. // 远程 /list 不返回"是否有子目录",保守给 true;
  97. // 前端真正展开时会再发一次 /list,行为上仍是懒加载
  98. HasChildren: true,
  99. })
  100. }
  101. return &types.DirChildrenResp{Nodes: nodes}, nil
  102. }
  103. // --- 调用远程 /list ------------------------------------------------------
  104. func (l *ListDirChildrenLogic) fetchRemoteList(ip, port, dir string) (*remoteListResp, error) {
  105. u := &url.URL{
  106. Scheme: "http",
  107. Host: net.JoinHostPort(ip, port),
  108. Path: "/list",
  109. }
  110. q := url.Values{}
  111. q.Set("path", dir)
  112. u.RawQuery = q.Encode()
  113. ctx, cancel := context.WithTimeout(l.ctx, 10*time.Second)
  114. defer cancel()
  115. httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
  116. if err != nil {
  117. return nil, apierr.BadRequest("构造远程请求失败: %v", err)
  118. }
  119. httpResp, err := http.DefaultClient.Do(httpReq)
  120. if err != nil {
  121. l.Errorf("call remote /list %s failed: %v", u.String(), err)
  122. return nil, apierr.BadRequest("调用远程接口失败: %v", err)
  123. }
  124. defer httpResp.Body.Close()
  125. // 保护性限制:一个目录条目正常远小于 8MB
  126. body, readErr := io.ReadAll(io.LimitReader(httpResp.Body, 8<<20))
  127. if readErr != nil {
  128. return nil, apierr.BadRequest("读取远程响应失败: %v", readErr)
  129. }
  130. switch httpResp.StatusCode {
  131. case http.StatusOK:
  132. // ok,继续解析
  133. case http.StatusBadRequest:
  134. // 远程 400 自带中文说明,原样透传给前端
  135. return nil, apierr.BadRequest("%s", extractRemoteMessage(body))
  136. case http.StatusMethodNotAllowed:
  137. return nil, apierr.BadRequest("远程服务拒绝了 GET 请求")
  138. default:
  139. return nil, apierr.BadRequest("远程接口异常(%d): %s", httpResp.StatusCode, extractRemoteMessage(body))
  140. }
  141. var out remoteListResp
  142. if err := json.Unmarshal(body, &out); err != nil {
  143. return nil, apierr.BadRequest("解析远程响应失败: %v", err)
  144. }
  145. return &out, nil
  146. }
  147. // 从远程错误响应中提取中文说明(兼容 message/msg/error 字段)
  148. func extractRemoteMessage(body []byte) string {
  149. if len(body) == 0 {
  150. return "远程接口返回空响应"
  151. }
  152. var wrap struct {
  153. Message string `json:"message"`
  154. Msg string `json:"msg"`
  155. Error string `json:"error"`
  156. }
  157. if json.Unmarshal(body, &wrap) == nil {
  158. switch {
  159. case wrap.Message != "":
  160. return wrap.Message
  161. case wrap.Msg != "":
  162. return wrap.Msg
  163. case wrap.Error != "":
  164. return wrap.Error
  165. }
  166. }
  167. return string(body)
  168. }
  169. // hasSubDir 判断目录下是否还有子目录(只读一层、找到一个即返回,开销可控)
  170. func hasSubDir(dir string) bool {
  171. subs, err := os.ReadDir(dir)
  172. if err != nil {
  173. return false
  174. }
  175. for _, s := range subs {
  176. if s.IsDir() {
  177. return true
  178. }
  179. }
  180. return false
  181. }