diff --git a/cmd/9p/main.go b/cmd/9p/main.go new file mode 100644 index 0000000..535a461 --- /dev/null +++ b/cmd/9p/main.go @@ -0,0 +1,348 @@ +// Command 9p is a complete 9P client for interacting with olliesrv. +// It replaces plan9port's 9p command with no external dependencies. +// +// Usage: +// +// 9p [-a addr] [-A aname] [-n] cmd args... +// +// Commands: +// +// read path read file contents to stdout +// write path write stdin to file +// rdwr path write stdin then read response (request-response files) +// stat path print file metadata +// ls [-l] [-d] path list directory contents +// create path create a file +// remove path remove a file +// mkdir path create a directory +package main + +import ( + "flag" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "time" + + "9fans.net/go/plan9" + "9fans.net/go/plan9/client" +) + +var ( + addr = flag.String("a", "", "server address (default: unix!$ns/ollie)") + aname = flag.String("A", "", "attach name (aname in Tattach)") + noauth = flag.Bool("n", false, "no authentication") +) + +func usage() { + fmt.Fprintf(os.Stderr, "usage: 9p [-a addr] [-A aname] [-n] cmd args...\n") + fmt.Fprintf(os.Stderr, "commands: read write rdwr stat ls create remove mkdir\n") + os.Exit(1) +} + +func fatalf(format string, args ...any) { + fmt.Fprintf(os.Stderr, "9p: "+format+"\n", args...) + os.Exit(1) +} + +func main() { + flag.Usage = usage + flag.Parse() + if flag.NArg() < 1 { + usage() + } + + cmd := flag.Arg(0) + args := flag.Args()[1:] + + switch cmd { + case "read": + if len(args) != 1 { + fatalf("usage: 9p read path") + } + cmdRead(args[0]) + case "write": + if len(args) != 1 { + fatalf("usage: 9p write path") + } + cmdWrite(args[0]) + case "rdwr": + if len(args) != 1 { + fatalf("usage: 9p rdwr path") + } + cmdRdwr(args[0]) + case "stat": + if len(args) != 1 { + fatalf("usage: 9p stat path") + } + cmdStat(args[0]) + case "ls": + cmdLs(args) + case "create": + if len(args) != 1 { + fatalf("usage: 9p create path") + } + cmdCreate(args[0]) + case "remove": + if len(args) != 1 { + fatalf("usage: 9p remove path") + } + cmdRemove(args[0]) + case "mkdir": + if len(args) != 1 { + fatalf("usage: 9p mkdir path") + } + cmdMkdir(args[0]) + default: + fatalf("unknown command: %s", cmd) + } +} + +func dial() *client.Fsys { + address := *addr + if address == "" { + ns := os.Getenv("NAMESPACE") + if ns == "" { + ns = client.Namespace() + } + if ns == "" { + fatalf("cannot determine namespace; use -a flag or set $NAMESPACE") + } + address = ns + "/ollie" + } + + var fsys *client.Fsys + var err error + if *aname != "" { + fsys, err = mountWithAname(address, *aname) + } else { + fsys, err = client.Mount("unix", address) + } + if err != nil { + fatalf("dial %s: %v", address, err) + } + return fsys +} + +// mountWithAname connects and attaches with a specific aname. +func mountWithAname(address, aname string) (*client.Fsys, error) { + conn, err := client.Dial("unix", address) + if err != nil { + return nil, err + } + fsys, err := conn.Attach(nil, getuser(), aname) + if err != nil { + conn.Close() + return nil, err + } + return fsys, nil +} + +func getuser() string { + if u := os.Getenv("USER"); u != "" { + return u + } + return "none" +} + +func cmdRead(path string) { + fsys := dial() + fid, err := fsys.Open(path, plan9.OREAD) + if err != nil { + fatalf("open %s: %v", path, err) + } + defer fid.Close() + if _, err := io.Copy(os.Stdout, fid); err != nil { + fatalf("read %s: %v", path, err) + } +} + +func cmdWrite(path string) { + fsys := dial() + fid, err := fsys.Open(path, plan9.OWRITE) + if err != nil { + fatalf("open %s: %v", path, err) + } + defer fid.Close() + if _, err := io.Copy(fid, os.Stdin); err != nil { + fatalf("write %s: %v", path, err) + } +} + +func cmdRdwr(path string) { + fsys := dial() + fid, err := fsys.Open(path, plan9.ORDWR) + if err != nil { + fatalf("open %s: %v", path, err) + } + defer fid.Close() + + // Write stdin + data, err := io.ReadAll(os.Stdin) + if err != nil { + fatalf("read stdin: %v", err) + } + if _, err := fid.Write(data); err != nil { + fatalf("write %s: %v", path, err) + } + + // Seek back to 0 and read response + if _, err := fid.Seek(0, 0); err != nil { + fatalf("seek %s: %v", path, err) + } + if _, err := io.Copy(os.Stdout, fid); err != nil { + fatalf("read %s: %v", path, err) + } +} + +func cmdStat(path string) { + fsys := dial() + dir, err := fsys.Stat(path) + if err != nil { + fatalf("stat %s: %v", path, err) + } + printDir(dir) +} + +func cmdLs(args []string) { + fs := flag.NewFlagSet("ls", flag.ExitOnError) + long := fs.Bool("l", false, "long format") + dirOnly := fs.Bool("d", false, "list directory itself, not contents") + fs.Parse(args) + + paths := fs.Args() + if len(paths) == 0 { + paths = []string{"/"} + } + + fsys := dial() + + for _, path := range paths { + if *dirOnly { + dir, err := fsys.Stat(path) + if err != nil { + fatalf("stat %s: %v", path, err) + } + if *long { + printDir(dir) + } else { + fmt.Println(dir.Name) + } + continue + } + + dir, err := fsys.Stat(path) + if err != nil { + fatalf("stat %s: %v", path, err) + } + + if dir.Mode&plan9.DMDIR == 0 { + // It's a file + if *long { + printDir(dir) + } else { + fmt.Println(dir.Name) + } + continue + } + + // It's a directory — list contents + fid, err := fsys.Open(path, plan9.OREAD) + if err != nil { + fatalf("open %s: %v", path, err) + } + dirs, err := fid.Dirreadall() + fid.Close() + if err != nil { + fatalf("dirread %s: %v", path, err) + } + if *long { + for _, d := range dirs { + printDir(d) + } + } else { + for _, d := range dirs { + fmt.Println(d.Name) + } + } + } +} + +func cmdCreate(path string) { + fsys := dial() + dir, name := splitPath(path) + + parent, err := fsys.Open(dir, plan9.OREAD) + if err != nil { + fatalf("open %s: %v", dir, err) + } + defer parent.Close() + + _, err = parent.Walk(name) + if err == nil { + fatalf("%s: already exists", path) + } + + fid, err := fsys.Create(path, plan9.OWRITE, 0666) + if err != nil { + fatalf("create %s: %v", path, err) + } + fid.Close() +} + +func cmdRemove(path string) { + fsys := dial() + if err := fsys.Remove(path); err != nil { + fatalf("remove %s: %v", path, err) + } +} + +func cmdMkdir(path string) { + fsys := dial() + fid, err := fsys.Create(path, plan9.OREAD, plan9.DMDIR|0755) + if err != nil { + fatalf("mkdir %s: %v", path, err) + } + fid.Close() +} + +func printDir(d *plan9.Dir) { + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0) + modeStr := modeString(d.Mode) + t := time.Unix(int64(d.Mtime), 0).Format("Jan _2 15:04") + fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\n", modeStr, d.Uid, d.Gid, d.Length, t, d.Name) + w.Flush() +} + +func modeString(mode plan9.Perm) string { + var buf [10]byte + if mode&plan9.DMDIR != 0 { + buf[0] = 'd' + } else { + buf[0] = '-' + } + const rwx = "rwx" + for i := 0; i < 3; i++ { + for j := 0; j < 3; j++ { + if mode&(1<