227 lines
5.4 KiB
Go
227 lines
5.4 KiB
Go
// ollie-httpgw - HTTP gateway for the ollie 9P server.
|
|
//
|
|
// Maps HTTP methods to 9P file operations:
|
|
//
|
|
// GET /path - read file content or list directory entries
|
|
// PUT /path - write request body to file
|
|
// POST /path - create file (name = last path element)
|
|
// DELETE /path - remove file
|
|
// HEAD /path - stat file (metadata in headers)
|
|
// PATCH /path - wstat (rename via JSON {"name": "newname"})
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
|
|
"9fans.net/go/plan9"
|
|
"9fans.net/go/plan9/client"
|
|
)
|
|
|
|
var (
|
|
listen = flag.String("listen", ":8080", "HTTP listen address")
|
|
service = flag.String("service", "ollie", "9P service name to connect to")
|
|
network = flag.String("net", "", "network type (unix, tcp); empty = use namespace")
|
|
addr = flag.String("addr", "", "9P server address; empty = use namespace service")
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
http.HandleFunc("/", handler)
|
|
log.Printf("ollie-httpgw listening on %s", *listen)
|
|
log.Fatal(http.ListenAndServe(*listen, nil))
|
|
}
|
|
|
|
func dial() (*client.Fsys, error) {
|
|
if *addr != "" {
|
|
n := *network
|
|
if n == "" {
|
|
n = "tcp"
|
|
}
|
|
return client.Mount(n, *addr)
|
|
}
|
|
return client.MountService(*service)
|
|
}
|
|
|
|
type dirEntry struct {
|
|
Name string `json:"name"`
|
|
Size uint64 `json:"size"`
|
|
Mode uint32 `json:"mode"`
|
|
Mtime uint32 `json:"mtime"`
|
|
IsDir bool `json:"is_dir"`
|
|
}
|
|
|
|
func dirToEntry(d *plan9.Dir) dirEntry {
|
|
return dirEntry{
|
|
Name: d.Name,
|
|
Size: d.Length,
|
|
Mode: uint32(d.Mode),
|
|
Mtime: d.Mtime,
|
|
IsDir: d.Mode&plan9.DMDIR != 0,
|
|
}
|
|
}
|
|
|
|
func setStatHeaders(w http.ResponseWriter, d *plan9.Dir) {
|
|
w.Header().Set("X-9p-Name", d.Name)
|
|
w.Header().Set("X-9p-Size", fmt.Sprintf("%d", d.Length))
|
|
w.Header().Set("X-9p-Mode", fmt.Sprintf("%o", d.Mode))
|
|
w.Header().Set("X-9p-Mtime", fmt.Sprintf("%d", d.Mtime))
|
|
if d.Mode&plan9.DMDIR != 0 {
|
|
w.Header().Set("X-9p-Type", "dir")
|
|
} else {
|
|
w.Header().Set("X-9p-Type", "file")
|
|
}
|
|
}
|
|
|
|
func handler(w http.ResponseWriter, r *http.Request) {
|
|
fsys, err := dial()
|
|
if err != nil {
|
|
http.Error(w, "9p connect: "+err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer fsys.Close()
|
|
|
|
p := strings.TrimPrefix(r.URL.Path, "/")
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
handleRead(w, fsys, p)
|
|
case http.MethodHead:
|
|
handleStat(w, fsys, p)
|
|
case http.MethodPut:
|
|
handleWrite(w, r, fsys, p)
|
|
case http.MethodPost:
|
|
handleCreate(w, r, fsys, p)
|
|
case http.MethodDelete:
|
|
handleRemove(w, fsys, p)
|
|
case http.MethodPatch:
|
|
handleWstat(w, r, fsys, p)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func handleRead(w http.ResponseWriter, fsys *client.Fsys, p string) {
|
|
d, err := fsys.Stat(p)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
setStatHeaders(w, d)
|
|
|
|
if d.Mode&plan9.DMDIR != 0 {
|
|
fid, err := fsys.Open(p, plan9.OREAD)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer fid.Close()
|
|
dirs, err := fid.Dirreadall()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
entries := make([]dirEntry, len(dirs))
|
|
for i, de := range dirs {
|
|
entries[i] = dirToEntry(de)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(entries)
|
|
return
|
|
}
|
|
|
|
fid, err := fsys.Open(p, plan9.OREAD)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer fid.Close()
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
io.Copy(w, fid)
|
|
}
|
|
|
|
func handleStat(w http.ResponseWriter, fsys *client.Fsys, p string) {
|
|
d, err := fsys.Stat(p)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
setStatHeaders(w, d)
|
|
}
|
|
|
|
func handleWrite(w http.ResponseWriter, r *http.Request, fsys *client.Fsys, p string) {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
fid, err := fsys.Open(p, plan9.OWRITE)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
n, err := fid.Write(body)
|
|
fid.Close()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]int{"bytes_written": n})
|
|
}
|
|
|
|
func handleCreate(w http.ResponseWriter, r *http.Request, fsys *client.Fsys, p string) {
|
|
dir := path.Dir(p)
|
|
name := path.Base(p)
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fid, err := fsys.Create(path.Join(dir, name), plan9.OWRITE, 0644)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if len(body) > 0 {
|
|
fid.Write(body)
|
|
}
|
|
fid.Close()
|
|
w.WriteHeader(http.StatusCreated)
|
|
}
|
|
|
|
func handleRemove(w http.ResponseWriter, fsys *client.Fsys, p string) {
|
|
if err := fsys.Remove(p); err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func handleWstat(w http.ResponseWriter, r *http.Request, fsys *client.Fsys, p string) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
d := &plan9.Dir{Name: req.Name}
|
|
if err := fsys.Wstat(p, d); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|