diff --git a/filewatch/filewatch.go b/filewatch/filewatch.go new file mode 100644 index 00000000..d00c0f78 --- /dev/null +++ b/filewatch/filewatch.go @@ -0,0 +1,122 @@ +package filewatch + +import ( + "io/ioutil" + "log" + "net/http" + "os" + "strconv" + "time" + + "github.com/gorilla/websocket" +) + +// Adapted from https://github.com/gorilla/websocket +// Copyright (c) 2013 The Gorilla WebSocket Authors +// https://github.com/gorilla/websocket/blob/master/examples/filewatch/main.go + +const ( + // Time allowed to write the file to the client. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the client. + pongWait = 60 * time.Second + + // Send pings to client with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Poll file for changes with this period. + filePeriod = 10 * time.Second +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +func readFileIfModified(filename string, lastMod time.Time) ([]byte, time.Time, error) { + fi, err := os.Stat(filename) + if err != nil { + return nil, lastMod, err + } + if !fi.ModTime().After(lastMod) { + return nil, lastMod, nil + } + p, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fi.ModTime(), err + } + return p, fi.ModTime(), nil +} + +func reader(ws *websocket.Conn) { + defer ws.Close() + ws.SetReadLimit(512) + ws.SetReadDeadline(time.Now().Add(pongWait)) + ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, _, err := ws.ReadMessage() + if err != nil { + break + } + } +} + +func writer(ws *websocket.Conn, filename string, lastMod time.Time) { + lastError := "" + pingTicker := time.NewTicker(pingPeriod) + fileTicker := time.NewTicker(filePeriod) + defer func() { + pingTicker.Stop() + fileTicker.Stop() + ws.Close() + }() + for { + select { + case <-fileTicker.C: + var p []byte + var err error + + p, lastMod, err = readFileIfModified(filename, lastMod) + + if err != nil { + if s := err.Error(); s != lastError { + lastError = s + p = []byte(lastError) + } + } else { + lastError = "" + } + + if p != nil { + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.WriteMessage(websocket.TextMessage, p); err != nil { + return + } + } + case <-pingTicker.C: + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + if _, ok := err.(websocket.HandshakeError); !ok { + log.Println(err) + } + return + } + + var lastMod time.Time + if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err == nil { + lastMod = time.Unix(0, n) + } + + go writer(ws, filename, lastMod) + reader(ws) +} diff --git a/maputnik.go b/maputnik.go index 2a3f516f..4428a011 100644 --- a/maputnik.go +++ b/maputnik.go @@ -6,6 +6,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/maputnik/desktop/filewatch" "github.com/urfave/cli" ) @@ -14,11 +15,39 @@ func main() { app.Name = "maputnik" app.Usage = "Server for integrating Maputnik locally" + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "file, f", + Usage: "Allow access to JSON style from web client", + }, + cli.BoolFlag{ + Name: "watch", + Usage: "Notify web client about JSON style file changes", + }, + } + app.Action = func(c *cli.Context) error { gui := http.FileServer(assetFS()) router := mux.NewRouter().StrictSlash(true) - //router.Path("/api/v1/apps").Methods("GET").HandlerFunc(foo) + + // Register websocket to notify we clients about file changes + filename := c.String("file") + + if filename != "" { + /* + router.Path("/files").Methods("GET").HandlerFunc(listFiles) + router.Path("/files/{filename}").Methods("PUT").HandlerFunc(saveFile) + */ + + if c.Bool("watch") { + router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + filewatch.ServeWebsocketFileWatcher(filename, w, r) + }) + } + + } + router.PathPrefix("/").Handler(http.StripPrefix("/", gui)) loggedRouter := handlers.LoggingHandler(os.Stdout, router)