package main // 本机目录的访问范围(docs/design/21-knowledge-sources.md §4.3)。 // 允许的根目录写死在代码中,不接受平台传来的配置。 import ( "os" "path/filepath" "sort" "strings" "golang.org/x/sys/unix" ) var allowedRoots = []string{"/home", "/mnt", "/media"} // ruleProblem 按 §4.3 检查一个已解析符号链接的绝对路径,通过时返回空字符串。 func ruleProblem(real string, roots []string) string { if !filepath.IsAbs(real) || filepath.Clean(real) != real { return "路径必须是规范的绝对路径" } root := "" for _, r := range roots { if real == r || strings.HasPrefix(real, r+"/") { root = r break } } if root == "" { return "只能选择 " + strings.Join(roots, "、") + " 下的目录" } if real == root { return "不能选择 " + root + " 本身,请选择其中的子目录" } rest := strings.Split(strings.TrimPrefix(real, root+"/"), "/") if root == "/home" && len(rest) == 1 { return "不能选择用户主目录本身,请选择其中的子目录" } if hasHidden(real) { return "不能选择隐藏目录(以 . 开头)或其中的目录" } return "" } func hasHidden(p string) bool { for _, seg := range strings.Split(p, "/") { if strings.HasPrefix(seg, ".") { return true } } return false } // inBrowseRange:逐级选择目录时可以进入的范围(允许的根目录本身与 home 目录本身也可以进入,但不能选择) func inBrowseRange(real string, roots []string) bool { if hasHidden(real) { return false } for _, r := range roots { if real == r || strings.HasPrefix(real, r+"/") { return true } } return false } // checkError 带状态码的检查失败 type checkError struct { State string // not_allowed | path_missing Message string } func (e *checkError) Error() string { return e.Message } // checkSource 检查要登记(或解析出的)本机目录,返回解析符号链接后的真实路径 func checkSource(p string, roots []string) (string, *checkError) { if !filepath.IsAbs(p) { return "", &checkError{"not_allowed", "请填写绝对路径"} } p = filepath.Clean(p) if hasHidden(p) { return "", &checkError{"not_allowed", "不能选择隐藏目录(以 . 开头)或其中的目录"} } real, err := filepath.EvalSymlinks(p) if err != nil { return "", &checkError{"path_missing", "目录不存在:" + p} } if msg := ruleProblem(real, roots); msg != "" { if real != p { msg += "(实际位置:" + real + ")" } return "", &checkError{"not_allowed", msg} } st, err := os.Stat(real) if err != nil { return "", &checkError{"path_missing", "目录不存在:" + p} } if !st.IsDir() { return "", &checkError{"not_allowed", "不是目录:" + p} } if unix.Access(real, unix.R_OK|unix.X_OK) != nil { return "", &checkError{"not_allowed", "挂载服务没有读取这个目录的权限:" + p} } return real, nil } type browseEntry struct { Name string `json:"name"` Path string `json:"path"` Selectable bool `json:"selectable"` Reason string `json:"reason,omitempty"` } type browseResult struct { Path string `json:"path"` Parent string `json:"parent"` Entries []browseEntry `json:"entries"` } const maxBrowseEntries = 2000 // browse 列出可选择的子目录:隐藏以 . 开头的,标出不能选择的及原因。path 为空时列出允许的根目录。 func browse(p string, roots []string) (*browseResult, *checkError) { if p == "" || p == "/" { res := &browseResult{Path: "", Parent: "", Entries: []browseEntry{}} for _, r := range roots { if st, err := os.Stat(r); err == nil && st.IsDir() { res.Entries = append(res.Entries, browseEntry{Name: r, Path: r, Reason: "请进入后选择子目录"}) } } return res, nil } if !filepath.IsAbs(p) { return nil, &checkError{"not_allowed", "请填写绝对路径"} } p = filepath.Clean(p) real, err := filepath.EvalSymlinks(p) if err != nil { return nil, &checkError{"path_missing", "目录不存在:" + p} } if hasHidden(p) || !inBrowseRange(real, roots) { return nil, &checkError{"not_allowed", "只能浏览 " + strings.Join(roots, "、") + " 下的目录"} } f, err := os.Open(real) if err != nil { return nil, &checkError{"not_allowed", "没有读取权限:" + p} } names, err := f.Readdirnames(-1) f.Close() if err != nil { return nil, &checkError{"not_allowed", "无法读取目录:" + p} } sort.Strings(names) parent := filepath.Dir(p) isRoot := false for _, r := range roots { if p == r { isRoot = true } } if isRoot { parent = "" } res := &browseResult{Path: p, Parent: parent, Entries: []browseEntry{}} for _, name := range names { if strings.HasPrefix(name, ".") { continue } child := filepath.Join(p, name) st, err := os.Stat(child) if err != nil || !st.IsDir() { continue } e := browseEntry{Name: name, Path: child, Selectable: true} childReal, err := filepath.EvalSymlinks(child) if err != nil { continue } if msg := ruleProblem(childReal, roots); msg != "" { e.Selectable, e.Reason = false, msg } else if unix.Access(childReal, unix.R_OK|unix.X_OK) != nil { e.Selectable, e.Reason = false, "没有读取权限" } if childReal != child && e.Selectable { e.Reason = "链接到 " + childReal } res.Entries = append(res.Entries, e) if len(res.Entries) >= maxBrowseEntries { break } } return res, nil }