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