diff --git a/plan9/fs/fs.go b/plan9/fs/fs.go new file mode 100644 index 0000000..e3dd211 --- /dev/null +++ b/plan9/fs/fs.go @@ -0,0 +1,252 @@ +// Package fs implements io/fs.FS for a 9p filesystem. +// An example is to run a local http.FileServer with a remote 9p system. +package fs + +import ( + "io" + "io/fs" + "strings" + "time" + + "9fans.net/go/plan9" + "9fans.net/go/plan9/client" +) + +// NewFS returns an io/fs.FS filesystem that wraps a 9p filesystem. +func NewFS(fsys *client.Fsys) fs.FS { + return fs9p{fsys: fsys} +} + +// MountWithNames attaches to the 9p filesystem at network!addr with the +// provided user and attach names and wraps an io/fs.FS filesystem around it. +// Assumes the 9p server does not require Auth. +func MountWithNames(network, addr, uname, aname string) (fs.FS, error) { + c, err := client.Dial(network, addr) + if err != nil { + return nil, err + } + fsys, err := c.Attach(nil, uname, aname) + if err != nil { + c.Close() + } + return fs9p{fsys: fsys}, nil +} + +// fs9p implements io/fs.FS. +type fs9p struct { + fsys *client.Fsys +} + +func (f fs9p) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{ + Op: "open", + Path: name, + Err: fs.ErrInvalid, + } + } + + fid, err := f.fsys.Open(name, plan9.OREAD) + if err != nil { + // error detection is based on freebsd/u9fs + werr := err + if strings.Contains(werr.Error(), "Permission denied") { + werr = fs.ErrPermission + } else if strings.Contains(werr.Error(), "No such file or directory") { + werr = fs.ErrNotExist + } + return nil, &fs.PathError{ + Op: "open", + Path: name, + Err: werr, + } + } + + if fid.Qid().Type&plan9.QTDIR > 0 { + return &dir9p{file9p: file9p{fid: fid}}, nil + } + return file9p{fid: fid}, nil +} + +// file9p implements io/fs.File. +type file9p struct { + fid *client.Fid +} + +func (f file9p) Stat() (fs.FileInfo, error) { + dir, err := f.fid.Stat() + if err != nil { + return nil, err + } + return fileInfo9p{sys: dir}, nil + +} + +func (f file9p) Read(b []byte) (int, error) { + return f.fid.Read(b) +} + +func (f file9p) Close() error { + return f.fid.Close() +} + +func (f file9p) ReadAt(p []byte, off int64) (n int, err error) { + return f.fid.ReadAt(p, off) +} + +func (f file9p) Seek(offset int64, whence int) (int64, error) { + return f.fid.Seek(offset, whence) +} + +// dir9p implements io/fs.ReadDirFile. +type dir9p struct { + file9p + + dirsRead []*plan9.Dir // directories read ahead by dirread +} + +func (d *dir9p) ReadDir(n int) ([]fs.DirEntry, error) { + var dirs []*plan9.Dir + var err error + + if n <= 0 { + dirs, err = d.fid.Dirreadall() + dirs = append(d.dirsRead, dirs...) // preserve directory order + d.dirsRead = d.dirsRead[:0] + } else { + for err == nil && len(d.dirsRead) < n { + dirs, err = d.fid.Dirread() + d.dirsRead = append(d.dirsRead, dirs...) // preserve directory order + } + if len(d.dirsRead) >= n { + dirs = d.dirsRead[0:n] + d.dirsRead = d.dirsRead[n:] + } else { + dirs = d.dirsRead + d.dirsRead = d.dirsRead[:0] + } + } + + if len(d.dirsRead) > 0 && err == io.EOF { + err = nil + } + + entries := make([]fs.DirEntry, len(dirs)) + for i, dir := range dirs { + entries[i] = dirEntry9p{sys: dir} + } + return entries, err +} + +// fileInfo9p implements io/fs.FileInfo. +type fileInfo9p struct { + sys *plan9.Dir +} + +func (f fileInfo9p) Name() string { + return f.sys.Name +} + +func (f fileInfo9p) Size() int64 { + // for directories size is implementation defined. Use 0 for portability. + if f.sys.Mode&plan9.DMDIR > 0 { + return 0 + } + + // 9p uses uint64 but FileInfo uses int64. For most practical cases, + // the conversion it OK. + return int64(f.sys.Length) +} + +func (f fileInfo9p) Mode() fs.FileMode { + // init mode to the permission bits and then set the others + mode := fs.FileMode(f.sys.Mode & 0777) + if f.sys.Mode&plan9.DMDIR > 0 { + mode |= fs.ModeDir + } + if f.sys.Mode&plan9.DMAPPEND > 0 { + mode |= fs.ModeAppend + } + if f.sys.Mode&plan9.DMEXCL > 0 { + mode |= fs.ModeExclusive + } + if f.sys.Mode&plan9.DMTMP > 0 { + mode |= fs.ModeTemporary + } + + // The following are not defined by 9p (http://9p.io/sys/man/5/INDEX.html) + // but are defined by the plan9 client + if f.sys.Mode&plan9.DMSYMLINK > 0 { + mode |= fs.ModeSymlink + } + if f.sys.Mode&plan9.DMDEVICE > 0 { + mode |= fs.ModeDevice + } + if f.sys.Mode&plan9.DMNAMEDPIPE > 0 { + mode |= fs.ModeNamedPipe + } + if f.sys.Mode&plan9.DMSOCKET > 0 { + mode |= fs.ModeSocket + } + if f.sys.Mode&plan9.DMSETUID > 0 { + mode |= fs.ModeSetuid + } + if f.sys.Mode&plan9.DMSETGID > 0 { + mode |= fs.ModeSetgid + } + + return mode +} + +func (f fileInfo9p) ModTime() time.Time { + return time.Unix(int64(f.sys.Mtime), 0) +} + +func (f fileInfo9p) IsDir() bool { + return f.sys.Mode&plan9.DMDIR > 0 +} + +func (f fileInfo9p) Sys() interface{} { + return f.sys +} + +// dirEntry9p implements io/fs.DirEntry. +type dirEntry9p struct { + sys *plan9.Dir +} + +func (d dirEntry9p) Name() string { + return d.sys.Name +} + +func (d dirEntry9p) IsDir() bool { + return d.sys.Mode&plan9.DMDIR > 0 +} + +func (d dirEntry9p) Info() (fs.FileInfo, error) { + return fileInfo9p{sys: d.sys}, nil +} + +func (d dirEntry9p) Type() fs.FileMode { + var mode fs.FileMode + if d.sys.Mode&plan9.DMDIR > 0 { + mode |= fs.ModeDir + } + + // The following are not defined by 9p (http://9p.io/sys/man/5/INDEX.html) + // but are defined by the plan9 client + if d.sys.Mode&plan9.DMSYMLINK > 0 { + mode |= fs.ModeSymlink + } + if d.sys.Mode&plan9.DMDEVICE > 0 { + mode |= fs.ModeDevice + } + if d.sys.Mode&plan9.DMNAMEDPIPE > 0 { + mode |= fs.ModeNamedPipe + } + if d.sys.Mode&plan9.DMSOCKET > 0 { + mode |= fs.ModeSocket + } + + return mode +} diff --git a/plan9/fs/fs_test.go b/plan9/fs/fs_test.go new file mode 100644 index 0000000..6702e34 --- /dev/null +++ b/plan9/fs/fs_test.go @@ -0,0 +1,66 @@ +//go:build u9fs && linux + +package fs + +import ( + "flag" + "os" + "os/exec" + "path" + "strings" + "syscall" + "testing" + "testing/fstest" + + "9fans.net/go/plan9/client" + "golang.org/x/sys/unix" +) + +// tests an io.FS backedup by a 9p server. Needs u9fs https://github.com/unofficial-mirror/u9fs +// Use the root flag to test any folder, for example +// go test -tags u9fs -root /home/user/images -exp 'gopher.png,glenda.png' -timeout 0 + +var root = flag.String("root", "./testdata", "the root tree to check") +var exp = flag.String("exp", "fortunes.txt", "a comma separated list of files expected to find in root tree") + +func TestFS(t *testing.T) { + execPath, err := exec.LookPath("u9fs") + check(t, err, "u9fs is not in PATH") + + // create a socket pair to connect the 9p client with a u9fs serving root + fds, err := unix.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + check(t, err, "failed to create socket pair") + defer unix.Close(fds[0]) + defer unix.Close(fds[1]) + + user := os.Getenv("USER") + + // first init the server + srv := os.NewFile(uintptr(fds[1]), "srv") + cmd := exec.Cmd{ + Path: execPath, + Args: []string{"u9fs", "-u", user, "-n", "-a", "none", + "-l", path.Join(t.TempDir(), "u9fs.log"), *root}, + Stdin: srv, + Stdout: srv, + } + check(t, cmd.Start(), "failed to start u9fs") + defer cmd.Process.Kill() + + // init the client last because server must be up to read Tversion + cli := os.NewFile(uintptr(fds[0]), "cli") + conn, err := client.NewConn(cli) + check(t, err, "failed to create client") + fsys, err := conn.Attach(nil, user, "") + check(t, err, "failed to attach client") + + // create and check the filesystem + err = fstest.TestFS(NewFS(fsys), strings.Split(*exp, ",")...) + check(t, err, "FS test failed") +} + +func check(t *testing.T, err error, msg string) { + if err != nil { + t.Fatalf("%s: %s", msg, err) + } +} diff --git a/plan9/fs/testdata/a/fortunesA.txt b/plan9/fs/testdata/a/fortunesA.txt new file mode 100644 index 0000000..2b6453d --- /dev/null +++ b/plan9/fs/testdata/a/fortunesA.txt @@ -0,0 +1 @@ +The whole UNIX operating system whose documentation fits in a chinese computer terminal! diff --git a/plan9/fs/testdata/b/fortunesB.txt b/plan9/fs/testdata/b/fortunesB.txt new file mode 100644 index 0000000..573f8be --- /dev/null +++ b/plan9/fs/testdata/b/fortunesB.txt @@ -0,0 +1 @@ +A watched terminal never prints. diff --git a/plan9/fs/testdata/fortunes.txt b/plan9/fs/testdata/fortunes.txt new file mode 100644 index 0000000..1225072 --- /dev/null +++ b/plan9/fs/testdata/fortunes.txt @@ -0,0 +1 @@ +You should be automatically redirected to the new page in 0 seconds.