messy go code
4 files changed, 358 insertions(+), 0 deletions(-)

A => .hgignore
A => main.go
A => metronome.go
A => server.go
A => .hgignore +2 -0
@@ 0,0 1,2 @@ 
+\.sw[po]$
+^watchdir$

          
A => main.go +17 -0
@@ 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)
+    }
+}

          
A => metronome.go +52 -0
@@ 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
+}

          
A => server.go +287 -0
@@ 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,
+    })
+}