package main // 本机目录的设备识别(docs/design/21-knowledge-sources.md §4.4): // 系统盘上的目录只按路径识别;其他设备上的目录记录文件系统 UUID 与设备内路径, // 每次解析时找到这个 UUID 当前挂在哪里,再拼出实际路径。 import ( "os" "path/filepath" "strconv" "strings" "golang.org/x/sys/unix" ) type mountEntry struct { Major, Minor uint32 Root string // 文件系统内被挂载的路径(bind mount 时不是 /) Point string FSType string Source string } func unescapeMountinfo(s string) string { if !strings.Contains(s, `\`) { return s } var b strings.Builder for i := 0; i < len(s); i++ { if s[i] == '\\' && i+3 < len(s) { if n, err := strconv.ParseUint(s[i+1:i+4], 8, 8); err == nil { b.WriteByte(byte(n)) i += 3 continue } } b.WriteByte(s[i]) } return b.String() } // parseMountinfo 解析 /proc/self/mountinfo func parseMountinfo(data string) []mountEntry { var out []mountEntry for _, line := range strings.Split(data, "\n") { f := strings.Fields(line) if len(f) < 10 { continue } sep := -1 for i := 6; i < len(f); i++ { if f[i] == "-" { sep = i break } } if sep < 0 || sep+2 >= len(f) { continue } mm := strings.SplitN(f[2], ":", 2) if len(mm) != 2 { continue } maj, err1 := strconv.ParseUint(mm[0], 10, 32) min, err2 := strconv.ParseUint(mm[1], 10, 32) if err1 != nil || err2 != nil { continue } out = append(out, mountEntry{ Major: uint32(maj), Minor: uint32(min), Root: unescapeMountinfo(f[3]), Point: unescapeMountinfo(f[4]), FSType: f[sep+1], Source: unescapeMountinfo(f[sep+2]), }) } return out } // system 提供识别所需的系统信息,测试中替换为模拟数据 type system interface { mountinfo() []mountEntry // uuids 返回 设备号 → 文件系统 UUID(来自 /dev/disk/by-uuid) uuids() map[uint64]string // stat 返回路径所在的设备号(不跟随最后的符号链接以外的行为与 stat 相同) devOf(path string) (uint64, error) } type realSystem struct{} func (realSystem) mountinfo() []mountEntry { b, err := os.ReadFile("/proc/self/mountinfo") if err != nil { return nil } return parseMountinfo(string(b)) } func (realSystem) uuids() map[uint64]string { out := map[uint64]string{} ents, err := os.ReadDir("/dev/disk/by-uuid") if err != nil { return out } for _, e := range ents { var st unix.Stat_t if unix.Stat(filepath.Join("/dev/disk/by-uuid", e.Name()), &st) == nil && st.Mode&unix.S_IFMT == unix.S_IFBLK { out[st.Rdev] = e.Name() } } return out } func (realSystem) devOf(path string) (uint64, error) { var st unix.Stat_t if err := unix.Stat(path, &st); err != nil { return 0, err } return st.Dev, nil } func devOfEntry(e mountEntry) uint64 { return unix.Mkdev(e.Major, e.Minor) } // within 判断 p 是否等于 base 或在 base 之下 func within(p, base string) bool { return p == base || base == "/" || strings.HasPrefix(p, base+"/") } // identity 本机目录的识别结果 type identity struct { Path string `json:"path"` UUID string `json:"uuid"` // 系统盘或无法识别的设备上为空 RelPath string `json:"relPath"` // 设备内路径 SystemDisk bool `json:"systemDisk"` MountPoint string `json:"mountPoint"` FSType string `json:"fsType"` Device string `json:"device"` } // identify 识别已检查通过的真实路径所在的文件系统 func identify(sys system, real string) identity { id := identity{Path: real} dev, err := sys.devOf(real) if err != nil { return id } rootDev, _ := sys.devOf("/") // 找到包含该路径、设备号相同、挂载点最长的条目 var best *mountEntry for _, e := range sys.mountinfo() { if devOfEntry(e) == dev && within(real, e.Point) && (best == nil || len(e.Point) >= len(best.Point)) { e := e best = &e } } if best != nil { id.MountPoint, id.FSType, id.Device = best.Point, best.FSType, best.Source } if dev == rootDev { id.SystemDisk = true return id } uuid := sys.uuids()[dev] if uuid == "" || best == nil { return id } id.UUID = uuid id.RelPath = filepath.Join(best.Root, strings.TrimPrefix(real, best.Point)) if !strings.HasPrefix(id.RelPath, "/") { id.RelPath = "/" + id.RelPath } return id } // localSource 平台登记的本机目录 type localSource struct { Path string `json:"path"` UUID string `json:"uuid"` RelPath string `json:"relPath"` } // locate 找到本机目录当前的实际路径。返回的路径还需要经过 checkSource。 func locate(sys system, src localSource) (string, *checkError) { if src.UUID == "" { return src.Path, nil } var dev uint64 found := false for d, u := range sys.uuids() { if u == src.UUID { dev, found = d, true break } } if !found { return "", &checkError{"device_missing", "设备未连接(文件系统 UUID " + src.UUID + ")"} } candidates := func() []string { var out []string for _, e := range sys.mountinfo() { if devOfEntry(e) == dev && within(src.RelPath, e.Root) { rest := strings.TrimPrefix(src.RelPath, e.Root) if e.Root == "/" { rest = src.RelPath } out = append(out, filepath.Join(e.Point, rest)) } } return out } list := candidates() if len(list) == 0 { // 可能是 x-systemd.automount:访问原路径触发自动挂载后再找一次 _, _ = sys.devOf(src.Path) list = candidates() } if len(list) == 0 { return "", &checkError{"device_missing", "设备已连接,但没有挂载到系统上(文件系统 UUID " + src.UUID + ")"} } // 优先选原来的路径,其次选第一个在允许范围内的 for _, p := range list { if p == src.Path { return p, nil } } for _, p := range list { if ruleProblem(p, allowedRoots) == "" { return p, nil } } return list[0], nil }