package main // 只读文件系统(docs/design/21-knowledge-sources.md §4.2): // - 以 . 开头的文件和目录不出现在列表中,按路径访问也返回"不存在"; // - 文件一律显示为所有人只读,目录为所有人可进入; // - 每次访问都先打开源目录并核对设备号、inode 号,再用 openat2(RESOLVE_BENEATH|RESOLVE_NO_SYMLINKS|RESOLVE_NO_XDEV) 打开, // 访问过程中目录被替换成符号链接也不会读到源目录以外的内容,也不会跨到源目录下挂载的其他文件系统; // - 资料库不可用时根目录照常存在,读取返回"输入/输出错误"。 import ( "context" "strings" "sync" "syscall" "time" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "golang.org/x/sys/unix" ) // source 一个挂载对应的源目录:真实路径及其设备号、inode 号;设备拔出、恢复时由 manager 更新。 // 不长期持有源目录的句柄,否则设备会一直"忙",无法卸载(安全弹出、自动挂载的空闲卸载都会失败)。 type source struct { mu sync.RWMutex path string // 为空表示不可用 dev uint64 ino uint64 gen uint64 // onMiss 不可用时有人访问,通知 manager 重新检查(异步、限速) onMiss func() } func newSource() *source { return &source{} } // set 更新源目录(path 为空表示不可用),返回是否有变化 func (s *source) set(path string, dev, ino uint64) bool { s.mu.Lock() defer s.mu.Unlock() if s.path == path && s.dev == dev && s.ino == ino { return false } s.path, s.dev, s.ino = path, dev, ino s.gen++ return true } func (s *source) close() { s.set("", 0, 0) } func (s *source) miss() syscall.Errno { if s.onMiss != nil { go s.onMiss() } return syscall.EIO } // openRoot 打开源目录并确认还是登记时的那个目录(设备拔出后,原路径可能露出挂载点下面的其他文件) func openRoot(path string, dev, ino uint64) (int, bool) { how := unix.OpenHow{Flags: unix.O_PATH | unix.O_DIRECTORY | unix.O_CLOEXEC, Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS} fd, err := unix.Openat2(unix.AT_FDCWD, path, &how) if err != nil { return -1, false } var st unix.Stat_t if unix.Fstat(fd, &st) != nil || st.Dev != dev || st.Ino != ino { unix.Close(fd) return -1, false } return fd, true } // openat 相对源目录打开 rel("." 表示源目录本身) func (s *source) openat(rel string, flags int) (int, syscall.Errno) { s.mu.RLock() path, dev, ino := s.path, s.dev, s.ino s.mu.RUnlock() if path == "" { return -1, s.miss() } root, ok := openRoot(path, dev, ino) if !ok { return -1, s.miss() } defer unix.Close(root) how := unix.OpenHow{Flags: uint64(flags | unix.O_CLOEXEC | unix.O_NOFOLLOW), Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_XDEV} for { fd, err := unix.Openat2(root, rel, &how) if err == unix.EINTR || err == unix.EAGAIN { continue } if err != nil { errno := err.(syscall.Errno) // 路径中出现符号链接、越界或跨到其他文件系统:对调用方表现为不存在 if errno == unix.ELOOP || errno == unix.EXDEV { return -1, syscall.ENOENT } return -1, errno } return fd, 0 } } func (s *source) generation() uint64 { s.mu.RLock() defer s.mu.RUnlock() return s.gen } type kbNode struct { fs.Inode src *source } var ( _ fs.NodeLookuper = (*kbNode)(nil) _ fs.NodeGetattrer = (*kbNode)(nil) _ fs.NodeOpener = (*kbNode)(nil) _ fs.NodeReaddirer = (*kbNode)(nil) _ fs.NodeReadlinker = (*kbNode)(nil) _ fs.NodeStatfser = (*kbNode)(nil) ) func (n *kbNode) rel() string { p := n.Path(n.Root()) if p == "" { return "." } return p } func (n *kbNode) child(name string) string { if r := n.rel(); r != "." { return r + "/" + name } return name } func hidden(name string) bool { return strings.HasPrefix(name, ".") } // statRel 不跟随符号链接地获取属性 func (n *kbNode) statRel(rel string) (unix.Stat_t, syscall.Errno) { var st unix.Stat_t fd, errno := n.src.openat(rel, unix.O_PATH) if errno != 0 { return st, errno } defer unix.Close(fd) if err := unix.Fstat(fd, &st); err != nil { return st, err.(syscall.Errno) } return st, 0 } func permFor(mode uint32) uint32 { switch mode & unix.S_IFMT { case unix.S_IFDIR: return 0o555 case unix.S_IFLNK: return 0o777 default: return 0o444 } } // inoFor 设备号参与计算,避免源目录下挂了其他文件系统时 inode 号冲突 func inoFor(st *unix.Stat_t) uint64 { return st.Ino ^ (uint64(unix.Major(st.Dev))<<56 | uint64(unix.Minor(st.Dev)&0xffff)<<40) } func fillAttr(st *unix.Stat_t, out *fuse.Attr) { out.Ino = inoFor(st) out.Size = uint64(st.Size) out.Blocks = uint64(st.Blocks) out.Blksize = uint32(st.Blksize) out.Atime, out.Atimensec = uint64(st.Atim.Sec), uint32(st.Atim.Nsec) out.Mtime, out.Mtimensec = uint64(st.Mtim.Sec), uint32(st.Mtim.Nsec) out.Ctime, out.Ctimensec = uint64(st.Ctim.Sec), uint32(st.Ctim.Nsec) out.Nlink = uint32(st.Nlink) out.Mode = st.Mode&unix.S_IFMT | permFor(st.Mode) out.Uid, out.Gid = 0, 0 } func (n *kbNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { if hidden(name) { return nil, syscall.ENOENT } st, errno := n.statRel(n.child(name)) if errno != 0 { return nil, errno } fillAttr(&st, &out.Attr) ch := n.NewInode(ctx, &kbNode{src: n.src}, fs.StableAttr{Mode: st.Mode & unix.S_IFMT, Ino: inoFor(&st), Gen: n.src.generation()}) return ch, 0 } func (n *kbNode) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { if f, ok := fh.(*kbFile); ok { return f.Getattr(ctx, out) } st, errno := n.statRel(n.rel()) if errno != 0 { if n.IsRoot() { // 不可用时根目录照常存在 now := uint64(time.Now().Unix()) out.Mode = unix.S_IFDIR | 0o555 out.Nlink = 2 out.Atime, out.Mtime, out.Ctime = now, now, now return 0 } return errno } fillAttr(&st, &out.Attr) return 0 } func (n *kbNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) { if flags&(unix.O_WRONLY|unix.O_RDWR|unix.O_TRUNC|unix.O_APPEND|unix.O_CREAT) != 0 { return nil, 0, syscall.EROFS } fd, errno := n.src.openat(n.rel(), unix.O_RDONLY) if errno != 0 { return nil, 0, errno } return &kbFile{fd: fd}, 0, 0 } func (n *kbNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { fd, errno := n.src.openat(n.rel(), unix.O_RDONLY|unix.O_DIRECTORY) if errno != 0 { return nil, errno } defer unix.Close(fd) var dirSt unix.Stat_t _ = unix.Fstat(fd, &dirSt) var list []fuse.DirEntry buf := make([]byte, 32*1024) for { nread, err := unix.Getdents(fd, buf) if err == unix.EINTR { continue } if err != nil { return nil, err.(syscall.Errno) } if nread <= 0 { break } for off := 0; off < nread; { reclen, ino, typ, name := parseDirent(buf[off:nread]) if reclen == 0 { break } off += reclen if name == "" || hidden(name) { continue } st := unix.Stat_t{Ino: ino, Dev: dirSt.Dev} list = append(list, fuse.DirEntry{Name: name, Mode: direntMode(typ), Ino: inoFor(&st)}) } } return fs.NewListDirStream(list), 0 } func (n *kbNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) { fd, errno := n.src.openat(n.rel(), unix.O_PATH) if errno != 0 { return nil, errno } defer unix.Close(fd) buf := make([]byte, 4096) k, err := unix.Readlinkat(fd, "", buf) if err != nil { return nil, err.(syscall.Errno) } return buf[:k], 0 } func (n *kbNode) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno { fd, errno := n.src.openat(".", unix.O_PATH) if errno != 0 { return 0 } defer unix.Close(fd) var s unix.Statfs_t if unix.Fstatfs(fd, &s) == nil { out.Blocks, out.Bfree, out.Bavail = s.Blocks, 0, 0 out.Files, out.Ffree = s.Files, 0 out.Bsize, out.Frsize = uint32(s.Bsize), uint32(s.Frsize) out.NameLen = uint32(s.Namelen) } return 0 } // kbFile 只读文件句柄 type kbFile struct { mu sync.Mutex fd int } var ( _ fs.FileReader = (*kbFile)(nil) _ fs.FileReleaser = (*kbFile)(nil) _ fs.FileGetattrer = (*kbFile)(nil) _ fs.FileLseeker = (*kbFile)(nil) ) func (f *kbFile) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { f.mu.Lock() defer f.mu.Unlock() return fuse.ReadResultFd(uintptr(f.fd), off, len(dest)), 0 } func (f *kbFile) Release(ctx context.Context) syscall.Errno { f.mu.Lock() defer f.mu.Unlock() if f.fd >= 0 { unix.Close(f.fd) f.fd = -1 } return 0 } func (f *kbFile) Getattr(ctx context.Context, out *fuse.AttrOut) syscall.Errno { f.mu.Lock() defer f.mu.Unlock() var st unix.Stat_t if err := unix.Fstat(f.fd, &st); err != nil { return err.(syscall.Errno) } fillAttr(&st, &out.Attr) return 0 } func (f *kbFile) Lseek(ctx context.Context, off uint64, whence uint32) (uint64, syscall.Errno) { f.mu.Lock() defer f.mu.Unlock() n, err := unix.Seek(f.fd, int64(off), int(whence)) if err != nil { return 0, err.(syscall.Errno) } return uint64(n), 0 } func direntMode(typ uint8) uint32 { switch typ { case unix.DT_DIR: return unix.S_IFDIR case unix.DT_LNK: return unix.S_IFLNK case unix.DT_FIFO: return unix.S_IFIFO case unix.DT_SOCK: return unix.S_IFSOCK case unix.DT_CHR: return unix.S_IFCHR case unix.DT_BLK: return unix.S_IFBLK default: return unix.S_IFREG } } // parseDirent 解析一条 linux_dirent64:d_ino(8) d_off(8) d_reclen(2) d_type(1) d_name func parseDirent(b []byte) (reclen int, ino uint64, typ uint8, name string) { if len(b) < 19 { return 0, 0, 0, "" } reclen = int(uint16(b[16]) | uint16(b[17])<<8) if reclen < 19 || reclen > len(b) { return 0, 0, 0, "" } for i := 0; i < 8; i++ { ino |= uint64(b[i]) << (8 * i) } typ = b[18] nb := b[19:reclen] if i := indexZero(nb); i >= 0 { nb = nb[:i] } name = string(nb) if name == "." || name == ".." { name = "" } return } func indexZero(b []byte) int { for i, c := range b { if c == 0 { return i } } return -1 } // mountFS 把源目录挂到 mountpoint func mountFS(mountpoint, name string, src *source) (*fuse.Server, error) { sec := time.Second root := &kbNode{src: src} return fs.Mount(mountpoint, root, &fs.Options{ EntryTimeout: &sec, AttrTimeout: &sec, NegativeTimeout: &sec, MountOptions: fuse.MountOptions{ AllowOther: true, FsName: name, Name: "agentkb", Options: []string{"ro", "nosuid", "nodev", "noexec"}, MaxReadAhead: 128 * 1024, }, }) }