package main import ( "context" "sync" "syscall" "9fans.net/go/plan9" "9fans.net/go/plan9/client" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" ) // p9Dir is a FUSE directory node backed by a 9P path. type p9Dir struct { fs.Inode fsys *client.Fsys path string // 9P path (empty string = root) } // p9File is a FUSE file node backed by a 9P path. type p9File struct { fs.Inode fsys *client.Fsys path string mu sync.Mutex } var _ = (fs.NodeGetattrer)((*p9Dir)(nil)) var _ = (fs.NodeReaddirer)((*p9Dir)(nil)) var _ = (fs.NodeLookuper)((*p9Dir)(nil)) var _ = (fs.NodeGetattrer)((*p9File)(nil)) var _ = (fs.NodeOpener)((*p9File)(nil)) var _ = (fs.NodeReader)((*p9File)(nil)) var _ = (fs.NodeWriter)((*p9File)(nil)) var _ = (fs.NodeSetattrer)((*p9File)(nil)) func p9path(base, name string) string { if base == "" { return name } return base + "/" + name } // --- p9Dir --- func (d *p9Dir) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { out.Mode = 0555 | syscall.S_IFDIR return 0 } func (d *p9Dir) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { path := d.path if path == "" { path = "/" } fid, err := d.fsys.Open(path, plan9.OREAD) if err != nil { return nil, syscall.EIO } defer fid.Close() dirs, err := fid.Dirreadall() if err != nil { return nil, syscall.EIO } entries := make([]fuse.DirEntry, len(dirs)) for i, dir := range dirs { m := uint32(dir.Mode) & 0777 if dir.Mode&plan9.DMDIR != 0 { m |= syscall.S_IFDIR } entries[i] = fuse.DirEntry{Name: dir.Name, Mode: m} } return fs.NewListDirStream(entries), 0 } func (d *p9Dir) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { child := p9path(d.path, name) dir, err := d.fsys.Stat(child) if err != nil { return nil, syscall.ENOENT } if dir.Mode&plan9.DMDIR != 0 { out.Mode = uint32(dir.Mode)&0777 | syscall.S_IFDIR node := &p9Dir{fsys: d.fsys, path: child} return d.NewInode(ctx, node, fs.StableAttr{Mode: syscall.S_IFDIR}), 0 } out.Mode = uint32(dir.Mode) & 0777 out.Size = dir.Length node := &p9File{fsys: d.fsys, path: child} return d.NewInode(ctx, node, fs.StableAttr{}), 0 } // --- p9File --- func (f *p9File) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { dir, err := f.fsys.Stat(f.path) if err != nil { return syscall.ENOENT } // Report all files as rw so the kernel doesn't block writes; // the 9P server enforces actual permissions. out.Mode = 0666 out.Size = dir.Length return 0 } func (f *p9File) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { // Accept truncate silently — 9P files are append/overwrite on write. out.Mode = 0666 return 0 } func (f *p9File) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) { return nil, fuse.FOPEN_DIRECT_IO, 0 } func (f *p9File) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { f.mu.Lock() defer f.mu.Unlock() fid, err := f.fsys.Open(f.path, plan9.OREAD) if err != nil { return nil, syscall.EIO } defer fid.Close() // The 9fans client doesn't support Seek; read from offset by reading and discarding. // For small offsets this is fine; large seeks on big files would need a smarter approach. if off > 0 { buf := make([]byte, min(off, 8192)) remaining := off for remaining > 0 { n := int64(len(buf)) if n > remaining { n = remaining } nr, err := fid.Read(buf[:n]) if err != nil || nr == 0 { break } remaining -= int64(nr) } } n, err := fid.Read(dest) if err != nil && n == 0 { return nil, syscall.EIO } return fuse.ReadResultData(dest[:n]), 0 } func (f *p9File) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) { f.mu.Lock() defer f.mu.Unlock() fid, err := f.fsys.Open(f.path, plan9.OWRITE) if err != nil { return 0, syscall.EACCES } n, err := fid.Write(data) if err != nil { fid.Close() return 0, syscall.EIO } if err := fid.Close(); err != nil { return 0, syscall.EACCES } return uint32(n), 0 } func min(a, b int64) int64 { if a < b { return a } return b }