listdirchildrenlogic.go 5.7 KB

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