summaryrefslogtreecommitdiff
path: root/api
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-04-21 18:46:40 -0700
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-04-21 18:46:40 -0700
commitd14605d1388aaa7cc9ef1c230eae5ba14c9cef44 (patch)
tree59fcda0fae7899ca577eed1f72d89bff17d5ad5d /api
downloadbackup-notify-d14605d1388aaa7cc9ef1c230eae5ba14c9cef44.tar.gz
backup-notify-d14605d1388aaa7cc9ef1c230eae5ba14c9cef44.zip
initial commit
Diffstat (limited to 'api')
-rw-r--r--api/backups/backups.go58
-rw-r--r--api/serve.go92
-rw-r--r--api/template/template.go73
-rw-r--r--api/types/types.go26
4 files changed, 249 insertions, 0 deletions
diff --git a/api/backups/backups.go b/api/backups/backups.go
new file mode 100644
index 0000000..d389582
--- /dev/null
+++ b/api/backups/backups.go
@@ -0,0 +1,58 @@
+package backups
+
+import (
+ "log"
+ "net/http"
+ "time"
+
+ "git.simponic.xyz/simponic/backup-notify/api/types"
+ "git.simponic.xyz/simponic/backup-notify/database"
+)
+
+func getHostStatusOverTime(backups []database.Backup) map[string][]bool {
+ hostnameBackups := make(map[string][]database.Backup)
+ statusOverTime := make(map[string][]bool)
+
+ if len(backups) == 0 {
+ return statusOverTime
+ }
+
+ firstReceivedBackup := backups[0].ReceivedOn
+ for _, backup := range backups {
+ if backup.ReceivedOn.Before(firstReceivedBackup) {
+ firstReceivedBackup = backup.ReceivedOn
+ }
+
+ if _, ok := hostnameBackups[backup.Hostname]; !ok {
+ hostnameBackups[backup.Hostname] = []database.Backup{}
+ }
+ hostnameBackups[backup.Hostname] = append(hostnameBackups[backup.Hostname], backup)
+ }
+
+ daysSinceFirstBackup := int(time.Since(firstReceivedBackup).Hours()/24) + 1
+
+ for hostname := range hostnameBackups {
+ statusOverTime[hostname] = make([]bool, daysSinceFirstBackup)
+ for _, backup := range hostnameBackups[hostname] {
+ dayReceivedOn := int(time.Since(backup.ReceivedOn).Hours() / 24)
+ statusOverTime[hostname][dayReceivedOn] = true
+ }
+ }
+
+ return statusOverTime
+}
+
+func ListBackupsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ backups, err := database.ListBackups(context.DBConn)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ hostStatusOverTime := getHostStatusOverTime(backups)
+ (*context.TemplateData)["HostStatusOverTime"] = hostStatusOverTime
+ return success(context, req, resp)
+ }
+}
diff --git a/api/serve.go b/api/serve.go
new file mode 100644
index 0000000..5772373
--- /dev/null
+++ b/api/serve.go
@@ -0,0 +1,92 @@
+package api
+
+import (
+ "database/sql"
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+
+ "git.simponic.xyz/simponic/backup-notify/api/backups"
+ "git.simponic.xyz/simponic/backup-notify/api/template"
+ "git.simponic.xyz/simponic/backup-notify/api/types"
+ "git.simponic.xyz/simponic/backup-notify/args"
+ "git.simponic.xyz/simponic/backup-notify/utils"
+)
+
+func LogRequestContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain {
+ context.Start = time.Now()
+ context.Id = utils.RandomId()
+
+ log.Println(req.Method, req.URL.Path, req.RemoteAddr, context.Id)
+ return success(context, req, resp)
+ }
+}
+
+func LogExecutionTimeContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain {
+ end := time.Now()
+ log.Println(context.Id, "took", end.Sub(context.Start))
+
+ return success(context, req, resp)
+ }
+}
+
+func HealthCheckContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain {
+ resp.WriteHeader(200)
+ resp.Write([]byte("healthy"))
+ return success(context, req, resp)
+ }
+}
+
+func FailurePassingContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(_success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ return failure(context, req, resp)
+ }
+}
+
+func IdContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain {
+ return success(context, req, resp)
+ }
+}
+
+func CacheControlMiddleware(next http.Handler, maxAge int) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ header := fmt.Sprintf("public, max-age=%d", maxAge)
+ w.Header().Set("Cache-Control", header)
+ next.ServeHTTP(w, r)
+ })
+}
+
+func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
+ mux := http.NewServeMux()
+
+ staticFileServer := http.FileServer(http.Dir(argv.StaticPath))
+ mux.Handle("GET /static/", http.StripPrefix("/static/", CacheControlMiddleware(staticFileServer, 3600)))
+
+ makeRequestContext := func() *types.RequestContext {
+ return &types.RequestContext{
+ DBConn: dbConn,
+ Args: argv,
+ TemplateData: &map[string]interface{}{},
+ }
+ }
+
+ mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(HealthCheckContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(backups.ListBackupsContinuation, backups.ListBackupsContinuation)(template.TemplateContinuation("backup_list.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ return &http.Server{
+ Addr: ":" + fmt.Sprint(argv.Port),
+ Handler: mux,
+ }
+}
diff --git a/api/template/template.go b/api/template/template.go
new file mode 100644
index 0000000..81cb9e2
--- /dev/null
+++ b/api/template/template.go
@@ -0,0 +1,73 @@
+package template
+
+import (
+ "bytes"
+ "errors"
+ "html/template"
+ "log"
+ "net/http"
+ "os"
+
+ "git.simponic.xyz/simponic/backup-notify/api/types"
+)
+
+func renderTemplate(context *types.RequestContext, templateName string, showBaseHtml bool) (bytes.Buffer, error) {
+ templatePath := context.Args.TemplatePath
+ basePath := templatePath + "/base_empty.html"
+ if showBaseHtml {
+ basePath = templatePath + "/base.html"
+ }
+
+ templateLocation := templatePath + "/" + templateName
+ tmpl, err := template.New("").ParseFiles(templateLocation, basePath)
+ if err != nil {
+ return bytes.Buffer{}, err
+ }
+
+ dataPtr := context.TemplateData
+ if dataPtr == nil {
+ dataPtr = &map[string]interface{}{}
+ }
+
+ data := *dataPtr
+
+ var buffer bytes.Buffer
+ err = tmpl.ExecuteTemplate(&buffer, "base", data)
+
+ if err != nil {
+ return bytes.Buffer{}, err
+ }
+ return buffer, nil
+}
+
+func TemplateContinuation(path string, showBase bool) types.Continuation {
+ return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ html, err := renderTemplate(context, path, true)
+ if errors.Is(err, os.ErrNotExist) {
+ resp.WriteHeader(404)
+ html, err = renderTemplate(context, "404.html", true)
+ if err != nil {
+ log.Println("error rendering 404 template", err)
+ resp.WriteHeader(500)
+ return failure(context, req, resp)
+ }
+
+ resp.Header().Set("Content-Type", "text/html")
+ resp.Write(html.Bytes())
+ return failure(context, req, resp)
+ }
+
+ if err != nil {
+ log.Println("error rendering template", err)
+ resp.WriteHeader(500)
+ resp.Write([]byte("error rendering template"))
+ return failure(context, req, resp)
+ }
+
+ resp.Header().Set("Content-Type", "text/html")
+ resp.Write(html.Bytes())
+ return success(context, req, resp)
+ }
+ }
+}
diff --git a/api/types/types.go b/api/types/types.go
new file mode 100644
index 0000000..1898867
--- /dev/null
+++ b/api/types/types.go
@@ -0,0 +1,26 @@
+package types
+
+import (
+ "database/sql"
+ "net/http"
+ "time"
+
+ "git.simponic.xyz/simponic/backup-notify/args"
+)
+
+type RequestContext struct {
+ DBConn *sql.DB
+ Args *args.Arguments
+
+ Id string
+ Start time.Time
+
+ TemplateData *map[string]interface{}
+}
+
+type BannerMessages struct {
+ Messages []string
+}
+
+type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain
+type ContinuationChain func(Continuation, Continuation) ContinuationChain