| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- // udeServer: 本地目录懒加载 —— 浏览 udeServer 所在 Linux 主机的文件系统。
- // 只列目录、不列文件;从根目录 "/" 开始,前端每展开一个节点请求一次该层子目录。
- // 安全边界:
- // 1. 路径归一化: 必须是绝对路径,filepath.Clean 防穿越
- // 2. 黑名单: /proc /sys /dev /run /boot 等系统目录不展示
- // 3. 挂载根目录(Mount.BaseDir, 如 /var/ude/mnt)不展示,避免把已挂载点当普通目录选
- // 4. 符号链接: DirEntry.IsDir() 对 symlink 返回 false,天然跳过,不跟随
- // 5. 权限不足目录: 返回空列表而非报错,树展开不中断
- package logic
- import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/url"
- "os"
- "path"
- "strings"
- "time"
- "udeServer/internal/apierr"
- "udeServer/internal/svc"
- "udeServer/internal/types"
- "github.com/zeromicro/go-zero/core/logx"
- )
- // 根层直接屏蔽的系统/虚拟目录(仅在这些目录位于 "/" 下时过滤)
- var dirBlacklist = map[string]bool{
- "proc": true,
- "sys": true,
- "dev": true,
- "run": true,
- "boot": true,
- "lost+found": true,
- "snap": true,
- }
- type ListDirChildrenLogic struct {
- logx.Logger
- ctx context.Context
- svcCtx *svc.ServiceContext
- }
- func NewListDirChildrenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListDirChildrenLogic {
- return &ListDirChildrenLogic{
- Logger: logx.WithContext(ctx),
- ctx: ctx,
- svcCtx: svcCtx,
- }
- }
- // --- 远程 /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)
- if p == "" {
- p = "/"
- }
- if !strings.HasPrefix(p, "/") {
- return nil, apierr.BadRequest("path 必须是绝对路径(以 / 开头)")
- }
- // 目标机是 Linux,统一用 path.Clean,避免依赖本机 OS 分隔符
- dir := path.Clean(p)
- if req.Ip == "" {
- return nil, apierr.BadRequest("ip 不能为空")
- }
- if req.Port == "" {
- return nil, apierr.BadRequest("port 不能为空")
- }
- // 2. 调用远程 /list 接口(只读一层,天然懒加载)
- remote, err := l.fetchRemoteList(req.Ip, req.Port, dir)
- if err != nil {
- return nil, err
- }
- // 3. 转换成前端节点结构(响应结构与原来保持一致)
- nodes := make([]types.LocalDirNode, 0, len(remote.Entries))
- for _, e := range remote.Entries {
- if !e.Dir {
- continue // 只要目录;文件/符号链接(dir:false)一律跳过
- }
- name := e.Name
- if dir == "/" && dirBlacklist[name] {
- continue // 根层黑名单系统目录
- }
- full := path.Join(dir, name)
- nodes = append(nodes, types.LocalDirNode{
- 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)
- if err != nil {
- return false
- }
- for _, s := range subs {
- if s.IsDir() {
- return true
- }
- }
- return false
- }
|