| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- 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,
- },
- })
- }
|