From 2259c78730e1c26f147fd4d3b6f8d29b391d2d04 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 21 Nov 2018 05:19:57 +0100 Subject: [PATCH] dashboard: came back online --- dashboard/.gitignore | 1 + dashboard/api/handler.go | 81 +++++++++++++++++++++ dashboard/api/router.go | 11 +++ dashboard/main.go | 117 +++++++++++++++++++++++++++++++ dashboard/static.go | 106 ++++++++++++++++++++++++++++ dashboard/static/js/dashboard.js | 76 +++++++++++--------- htdocs-dashboard | 1 + 7 files changed, 359 insertions(+), 34 deletions(-) create mode 100644 dashboard/.gitignore create mode 100644 dashboard/api/handler.go create mode 100644 dashboard/api/router.go create mode 100644 dashboard/main.go create mode 100644 dashboard/static.go create mode 120000 htdocs-dashboard diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 00000000..ef360c84 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1 @@ +dashboard \ No newline at end of file diff --git a/dashboard/api/handler.go b/dashboard/api/handler.go new file mode 100644 index 00000000..e1443268 --- /dev/null +++ b/dashboard/api/handler.go @@ -0,0 +1,81 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "github.com/julienschmidt/httprouter" +) + +type DispatchFunction func(httprouter.Params, []byte) (interface{}, error) + +func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + if addr := r.Header.Get("X-Forwarded-For"); addr != "" { + r.RemoteAddr = addr + } + log.Printf("%s \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent()) + + // Read the body + if r.ContentLength < 0 || r.ContentLength > 6553600 { + http.Error(w, fmt.Sprintf("{errmsg:\"Request too large or request size unknown\"}"), http.StatusRequestEntityTooLarge) + return + } + var body []byte + if r.ContentLength > 0 { + tmp := make([]byte, 1024) + for { + n, err := r.Body.Read(tmp) + for j := 0; j < n; j++ { + body = append(body, tmp[j]) + } + if err != nil || n <= 0 { + break + } + } + } + + var ret interface{} + var err error = nil + + ret, err = f(ps, body) + + // Format response + resStatus := http.StatusOK + if err != nil { + ret = map[string]string{"errmsg": err.Error()} + resStatus = http.StatusBadRequest + log.Println(r.RemoteAddr, resStatus, err.Error()) + } + + if ret == nil { + ret = map[string]string{"errmsg": "Page not found"} + resStatus = http.StatusNotFound + } + + w.Header().Set("X-FIC-Time", fmt.Sprintf("%f", float64(time.Now().UnixNano()/1000)/1000000)) + + if str, found := ret.(string); found { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resStatus) + io.WriteString(w, str) + } else if bts, found := ret.([]byte); found { + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", "attachment") + w.Header().Set("Content-Transfer-Encoding", "binary") + w.WriteHeader(resStatus) + w.Write(bts) + } else if j, err := json.Marshal(ret); err != nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", err), http.StatusInternalServerError) + } else { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resStatus) + w.Write(j) + } + } +} diff --git a/dashboard/api/router.go b/dashboard/api/router.go new file mode 100644 index 00000000..a6bd873b --- /dev/null +++ b/dashboard/api/router.go @@ -0,0 +1,11 @@ +package api + +import ( + "github.com/julienschmidt/httprouter" +) + +var router = httprouter.New() + +func Router() *httprouter.Router { + return router +} diff --git a/dashboard/main.go b/dashboard/main.go new file mode 100644 index 00000000..0319a447 --- /dev/null +++ b/dashboard/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "path/filepath" + "strings" + "syscall" + + "srs.epita.fr/fic-server/dashboard/api" + "srs.epita.fr/fic-server/libfic" + "srs.epita.fr/fic-server/settings" +) + +var StaticDir string +var TeamsDir string + +type ResponseWriterPrefix struct { + real http.ResponseWriter + prefix string +} + +func (r ResponseWriterPrefix) Header() http.Header { + return r.real.Header() +} + +func (r ResponseWriterPrefix) WriteHeader(s int) { + if v, exists := r.real.Header()["Location"]; exists { + r.real.Header().Set("Location", r.prefix+v[0]) + } + r.real.WriteHeader(s) +} + +func (r ResponseWriterPrefix) Write(z []byte) (int, error) { + return r.real.Write(z) +} + +func StripPrefix(prefix string, h http.Handler) http.Handler { + if prefix == "" { + return h + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if prefix != "/" && r.URL.Path == "/" { + http.Redirect(w, r, prefix+"/", http.StatusFound) + } else if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) { + r2 := new(http.Request) + *r2 = *r + r2.URL = new(url.URL) + *r2.URL = *r.URL + r2.URL.Path = p + h.ServeHTTP(ResponseWriterPrefix{w, prefix}, r2) + } else { + h.ServeHTTP(w, r) + } + }) +} + +func main() { + // Read parameters from command line + var bind = flag.String("bind", "127.0.0.1:8082", "Bind port/socket") + var baseURL = flag.String("baseurl", "/", "URL prepended to each URL") + flag.StringVar(&StaticDir, "static", "./htdocs-dashboard/", "Directory containing static files") + flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files") + flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where load and save settings") + flag.Parse() + + log.SetPrefix("[public] ") + + // Sanitize options + var err error + log.Println("Checking paths...") + if StaticDir, err = filepath.Abs(StaticDir); err != nil { + log.Fatal(err) + } + if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil { + log.Fatal(err) + } + if settings.SettingsDir, err = filepath.Abs(settings.SettingsDir); err != nil { + log.Fatal(err) + } + if *baseURL != "/" { + tmp := path.Clean(*baseURL) + baseURL = &tmp + } else { + tmp := "" + baseURL = &tmp + } + + // Prepare graceful shutdown + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) + + srv := &http.Server{ + Addr: *bind, + Handler: StripPrefix(*baseURL, api.Router()), + } + + // Serve content + go func() { + log.Fatal(srv.ListenAndServe()) + }() + log.Println(fmt.Sprintf("Ready, listening on %s", *bind)) + + // Wait shutdown signal + <-interrupt + + log.Print("The service is shutting down...") + srv.Shutdown(context.Background()) + log.Println("done") +} diff --git a/dashboard/static.go b/dashboard/static.go new file mode 100644 index 00000000..ab96b156 --- /dev/null +++ b/dashboard/static.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "net/http" + "path" + "time" + + "srs.epita.fr/fic-server/dashboard/api" + "srs.epita.fr/fic-server/settings" + + "github.com/julienschmidt/httprouter" +) + +func init() { + api.Router().GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, "index.html")) + }) + + api.Router().GET("/css/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + api.Router().GET("/fonts/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + api.Router().GET("/img/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + api.Router().GET("/js/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + api.Router().GET("/views/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + + api.Router().GET("/events.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "events.json")) + }) + api.Router().GET("/my.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "my.json")) + }) + api.Router().GET("/stats.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "stats.json")) + }) + api.Router().GET("/settings.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("X-FIC-Time", fmt.Sprintf("%f", float64(time.Now().UnixNano()/1000)/1000000)) + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(settings.SettingsDir, settings.SettingsFile)) + }) + api.Router().GET("/teams.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "teams.json")) + }) + api.Router().GET("/themes.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "themes.json")) + }) + + api.Router().GET("/public.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public.json")) + }) + api.Router().GET("/public0.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public0.json")) + }) + api.Router().GET("/public1.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public1.json")) + }) + api.Router().GET("/public2.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public2.json")) + }) + api.Router().GET("/public3.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public3.json")) + }) + api.Router().GET("/public4.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public4.json")) + }) + api.Router().GET("/public5.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public5.json")) + }) + api.Router().GET("/public6.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public6.json")) + }) + api.Router().GET("/public7.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public7.json")) + }) + api.Router().GET("/public8.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public8.json")) + }) + api.Router().GET("/public9.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, path.Join(TeamsDir, "public", "public9.json")) + }) +} diff --git a/dashboard/static/js/dashboard.js b/dashboard/static/js/dashboard.js index 8d72c0fb..bce24bcd 100644 --- a/dashboard/static/js/dashboard.js +++ b/dashboard/static/js/dashboard.js @@ -21,52 +21,56 @@ String.prototype.capitalize = function() { angular.module("FICApp") .controller("TimeController", function($scope, $rootScope, $http, $timeout) { - $scope.time = {}; - var initTime = function() { - $timeout.cancel($scope.cbi); - $scope.cbi = $timeout(initTime, 10000); - $http.get("/time.json").then(function(response) { - var time = response.data; - console.log("upd time"); - time.he = (new Date()).getTime(); - sessionStorage.userService = angular.toJson(time); + $rootScope.time = {}; + $rootScope.recvTime = function(response) { + sessionStorage.userService = angular.toJson({ + "cu": Math.floor(response.headers("x-fic-time") * 1000), + "he": (new Date()).getTime(), }); - }; - initTime(); + } function updTime() { $timeout.cancel($scope.cb); $scope.cb = $timeout(updTime, 1000); - if (sessionStorage.userService) { + if (sessionStorage.userService && $rootScope.settings) { var time = angular.fromJson(sessionStorage.userService); - var srv_cur = (Date.now() + (time.cu * 1000 - time.he)) / 1000; - var remain = time.du; - if (time.st == Math.floor(srv_cur)) { - $scope.refresh(true); + var settings = $rootScope.settings; + var srv_cur = new Date(Date.now() + (time.cu - time.he)); + + if (Math.floor(settings.start / 1000) == Math.floor(srv_cur / 1000)) { + $rootScope.refresh(true); + } + + var remain = 0; + if (settings.start == 0) { + $rootScope.time = {}; + return + } else if (settings.start > srv_cur) { + $rootScope.startIn = Math.floor((settings.start - srv_cur) / 1000); + remain = settings.end - settings.start; + } else if (settings.end > srv_cur) { $rootScope.startIn = 0; + remain = settings.end - srv_cur; } - if (time.st > 0 && time.st <= srv_cur) { - remain = time.st + time.du - srv_cur; - } else if (time.st > 0) { - $rootScope.startAt = time.st; - } + + remain = remain / 1000; + if (remain < 0) { remain = 0; - $scope.time.end = true; - $scope.time.expired = true; + $rootScope.time.end = true; + $rootScope.time.expired = true; } else if (remain < 60) { - $scope.time.end = false; - $scope.time.expired = true; + $rootScope.time.end = false; + $rootScope.time.expired = true; } else { - $scope.time.end = false; - $scope.time.expired = false; + $rootScope.time.end = false; + $rootScope.time.expired = false; } - $scope.time.start = time.st * 1000; - $scope.time.duration = time.du; - $scope.time.remaining = remain; - $scope.time.hours = Math.floor(remain / 3600); - $scope.time.minutes = Math.floor((remain % 3600) / 60); - $scope.time.seconds = Math.floor(remain % 60); + + $rootScope.time.remaining = remain; + $rootScope.time.hours = Math.floor(remain / 3600); + $rootScope.time.minutes = Math.floor((remain % 3600) / 60); + $rootScope.time.seconds = Math.floor(remain % 60); } } updTime(); @@ -157,7 +161,11 @@ angular.module("FICApp") $scope.stats = response.data; }); $http.get("/settings.json").then(function(response) { - $scope.settings = response.data; + $rootScope.recvTime(response); + response.data.start = new Date(response.data.start); + response.data.end = new Date(response.data.end); + response.data.generation = new Date(response.data.generation); + $rootScope.settings = response.data; }); $http.get("/themes.json").then(function(response) { if ($scope.lastthemeetag != undefined && $scope.lastthemeetag == response.headers().etag) diff --git a/htdocs-dashboard b/htdocs-dashboard new file mode 120000 index 00000000..4f97701f --- /dev/null +++ b/htdocs-dashboard @@ -0,0 +1 @@ +dashboard/static \ No newline at end of file