| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- 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
- }
|