@@ 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,
+ })
+}