# HG changeset patch # User sqwishy # Date 1383413284 25200 # Sat Nov 02 10:28:04 2013 -0700 # Node ID d4bb60adc9acb0cbee28e1ee4f4fcbc9e937dcdb # Parent 0000000000000000000000000000000000000000 messy go code diff --git a/.hgignore b/.hgignore new file mode 100644 --- /dev/null +++ b/.hgignore @@ -0,0 +1,2 @@ +\.sw[po]$ +^watchdir$ diff --git a/main.go b/main.go new file mode 100644 --- /dev/null +++ b/main.go @@ -0,0 +1,17 @@ +package main + +import _"os" +import "log" +import _"fmt" +import "net/http" + +func main() { + web, err := NewMetronomeHandler("watchdir") + if err != nil { + log.Fatal(err) + } + log.Println("Loaded metronome, beginning to serve webstuffs") + if err := http.ListenAndServe(":8123", web); err != nil { + log.Fatal(err) + } +} diff --git a/metronome.go b/metronome.go new file mode 100644 --- /dev/null +++ b/metronome.go @@ -0,0 +1,52 @@ +package main + +import "io/ioutil" + +const ( + EventCreate = "create" + EventModify = "modify" + EventDelete = "delete" +) + +type EntityPath struct { + WebPath string + FSPath string +} + +type Request struct { + Subscribe bool + Path string +} + +type Error struct { + Message string + Details string +} + +type State struct { + Event string `json:",omitempty"` + Path string + Payload interface{} +} + +type DelayedCachedReader struct { + Path string + IsLoaded bool + data []byte +} + +func NewDelayedCachedReader(path string) *DelayedCachedReader { + return &DelayedCachedReader{path, false, nil} +} + +func (r *DelayedCachedReader) MarshalJSON() ([]byte, error) { + if r.IsLoaded == false { + data, err := ioutil.ReadFile(r.Path) + if err != nil { + return nil, err + } + r.IsLoaded = true + r.data = data + } + return r.data, nil +} diff --git a/server.go b/server.go new file mode 100644 --- /dev/null +++ b/server.go @@ -0,0 +1,287 @@ +package main + +import "net/http" +import "os" +import "log" +import "io/ioutil" +import "fmt" +import "strings" +import "path/filepath" + +import "code.google.com/p/go.net/websocket" +import "github.com/howeyc/fsnotify" + +func givePathSuffix(s string) string { + if s[len(s)-1] == os.PathSeparator { + return s + } + return s + string([]rune{os.PathSeparator}) +} + +type MetronomeHandler struct { + *http.ServeMux + watchdir string + watcher *fsnotify.Watcher + subs map[string][]*websocket.Conn +} + +func NewMetronomeHandler(watchdir string) (*MetronomeHandler, error) { + watchdir, err := filepath.Abs(watchdir) + if err != nil { + return nil, err + } + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + if err = watcher.Watch(watchdir); err != nil { + return nil, err + } + + h := &MetronomeHandler{ + ServeMux: http.NewServeMux(), + watchdir: watchdir, + watcher: watcher, + subs: make(map[string][]*websocket.Conn), + } + h.ServeMux.Handle("/", http.FileServer(http.Dir("static"))) + h.ServeMux.Handle("/ws", websocket.Handler(h.serveWS)) + go h.handleFsEvents() + return h, nil +} + +func (h *MetronomeHandler) toFsPath(path string) (string, error) { + path, err := filepath.Abs(filepath.Join(h.watchdir, path)) + if err != nil { + return "", err + } + //todo filepath.EvalSymlinks? + if strings.HasPrefix(path, h.watchdir) == false { + return "", fmt.Errorf("Path does not appear to be inside a valid directory") + } + return path, nil +} + +func dirListing(fspath string) ([]string, error) { + listing := make([]string, 0) + fis, err := ioutil.ReadDir(fspath) + if err != nil { + return nil, err + } else { + for _, fi := range fis { + item := fi.Name() + log.Println("listing", item) + if fi.IsDir() { + item = givePathSuffix(item) + } + listing = append(listing, item) + } + } + return listing, nil +} + +func (h *MetronomeHandler) serveWS(ws *websocket.Conn) { + defer ws.Close() + for { + log.Println("serving:", ws) + if err := h.readWS(ws); err != nil { + log.Println("serveWS error:", err) + break + } + } +} + +func (h *MetronomeHandler) readWS(ws *websocket.Conn) error { + r := new(Request) + if err := websocket.JSON.Receive(ws, r); err != nil { + return websocket.JSON.Send(ws, &Error{ + Message: "Invalid request", + Details: err.Error(), + }) + } + log.Println(r) + if r.Path == "" { + return websocket.JSON.Send(ws, &Error{ + Message: "Invalid path in request", + Details: "Path must not be empty", + }) + } + fspath, err := h.toFsPath(r.Path) + if err != nil { + return websocket.JSON.Send(ws, &Error{ + Message: "Invalid path in request", + Details: err.Error(), + }) + } + subs, ok := h.subs[r.Path] + if ok == false { + subs = make([]*websocket.Conn, 0) + } + subs = append(subs, ws) + h.subs[r.Path] = subs + + // Get Directory listing + if r.Path[len(r.Path)-1] == os.PathSeparator { + listing, err := dirListing(fspath) + if err != nil { + log.Printf("error getting listing of %v: %v", fspath, err) + return websocket.JSON.Send(ws, &Error{ + Message: "Server failed to get a directory listing", + Details: err.Error(), + }) + } + return websocket.JSON.Send(ws, &State{ + Path: r.Path, + Payload: listing, + }) + } + + // Or read a file + return websocket.JSON.Send(ws, &State{ + Path: r.Path, + Payload: NewDelayedCachedReader(fspath), + }) +} + +func (h *MetronomeHandler) handleFsEvents() { + for { + select { + case ev := <-h.watcher.Event: + path := ev.Name[len(h.watchdir):] + log.Println("event:", ev, path) + if ev.IsDelete() || ev.IsRename() { + if path[len(path)-1] == os.PathSeparator { + h.dirRemoved(path) + } else { + // Not sure if it's a directory or a file I guess + h.dirRemoved(path) + h.fileRemoved(path) + } + } else if ev.IsModify() { + h.fileModified(path) + } else if ev.IsCreate() { + fi, err := os.Stat(ev.Name) + if err != nil { + log.Printf("Failure getting stat of %v: %v\n", path, err) + break + } + if fi.IsDir() { + h.dirAdded(path) + } else { + h.fileAdded(path) + } + } + case err := <-h.watcher.Error: + log.Println("error:", err) + } + } +} + +func notifyConn(conn *websocket.Conn, msg interface{}) error { + log.Printf("notifying %v with %+v", conn, msg) + return websocket.JSON.Send(conn, msg) +} + +func (h *MetronomeHandler) notifyConns(path string, msg interface{}) { + conns, ok := h.subs[path] + if ok { + for _, conn := range conns { + if err := notifyConn(conn, msg); err != nil { + log.Println("Error while notifying a connection", err) + } + } + } +} + +func (h *MetronomeHandler) fileAdded(path string) { + fspath, err := h.toFsPath(path) + if err != nil { + panic(err) + } + h.notifyConns(path, &State{ + Event: EventCreate, + Path: path, + Payload: NewDelayedCachedReader(fspath), + }) + h.dirModified(filepath.Dir(path)); +} + +func (h *MetronomeHandler) fileModified(path string) { + fspath, err := h.toFsPath(path) + if err != nil { + panic(err) + } + h.notifyConns(path, &State{ + Event: EventDelete, + Path: path, + Payload: NewDelayedCachedReader(fspath), + }) +} + +func (h *MetronomeHandler) fileRemoved(path string) { + h.notifyConns(path, &State{ + Event: EventDelete, + Path: path, + }) + h.dirModified(filepath.Dir(path)); +} + +func (h *MetronomeHandler) dirAdded(path string) { + fspath, err := h.toFsPath(path) + if err != nil { + panic(err) + } + path = givePathSuffix(path) + h.notifyConns(path, &State{ + Event: EventCreate, + Path: path, + }) + filepath.Walk(fspath, func(path string, info os.FileInfo, err error) error { + log.Println("walking", path) + if err != nil { + log.Println("error while walking into new directory: ", err) + } else { + if info.IsDir() { + h.dirAdded(path) + } else { + h.fileAdded(path) + } + } + return nil + }) +} + +func (h *MetronomeHandler) dirModified(path string) { + fspath, err := h.toFsPath(path) + if err != nil { + panic(err) + } + path = givePathSuffix(path) + listing, err := dirListing(fspath) + if err != nil { + log.Printf("error getting listing of %v: %v", path, err) + return + } + h.notifyConns(path, &State{ + Event: EventDelete, + Path: path, + Payload: listing, + }) +} + +func (h *MetronomeHandler) dirRemoved(path string) { + path = givePathSuffix(path) + for name, _ := range h.subs { + if strings.HasPrefix(name, path) { + if name[len(name)-1] == os.PathSeparator { + h.dirRemoved(name) + } else { + h.fileRemoved(name) + } + } + } + h.notifyConns(path, &State{ + Event: EventDelete, + Path: path, + }) +}