aboutsummaryrefslogtreecommitdiff
path: root/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'server.go')
-rw-r--r--server.go289
1 files changed, 289 insertions, 0 deletions
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..6eb46b6
--- /dev/null
+++ b/server.go
@@ -0,0 +1,289 @@
+package main
+
+// Ideez instance code.
+
+import (
+ _ "embed"
+ "encoding/gob"
+ "errors"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "runtime/debug"
+ "strconv"
+ "time"
+)
+
+const (
+ SERVER_ID_BYTES = 8
+)
+
+// This is the Object that should be paired to the Ideas.
+type Ideez struct {
+ Id string
+ Name string
+}
+
+// Represents an idea
+// LastUpdated is a formatted date string
+// out of 5 rating of the idea
+// formatted time string with DateLayout
+type Idea struct {
+ Id int
+ Title string
+ Text string
+ Author string
+ LastUpdated string
+}
+
+// Data passed to the ideas_html template
+type IdeasPageData struct {
+ Name string
+ Ideas []Idea
+ Error string
+ RoutePrefix string
+}
+
+var (
+ //go:embed templates/ideas.html
+ ideas_html string
+ //go:embed templates/edit.html
+ idea_edit_html string
+ // layout for how the date should be output in html
+ DateLayout string = "02/01/2006 on 15:04"
+)
+
+func GetVersion() string {
+ buildinfo, ok := debug.ReadBuildInfo()
+ if !ok {
+ panic("Could not read buildinfo to know package version.")
+ }
+ return buildinfo.Main.Version
+}
+
+// This function intializes a server for use it
+// 1. Imports the data file for its ID, or creates one if it does not exist
+// 2. Creates a SIGINT handler to save the data
+// 3. Creates the html templates
+// 4. Registers the routes to mux passed
+// Returns a new server so that the manager can add it to the list of servers
+func NewIdeez(mux *http.ServeMux, name, id string) Ideez {
+ var logger = log.New(os.Stdout, "NewIdeez: ", log.Ldate)
+ logger.Printf("New server %s[%s]\n", name, id)
+
+ // Ideas for this server
+ var Ideas []Idea
+ // version number used for debugging compatibility with the .data file
+ version := GetVersion()
+ // Prefix that should be prepended before each route
+ routePrefix := "/server/" + id
+ // File for persistent storage
+ dataFilename := DataDir + "/" + id + ".data"
+ // Counter that goes up each time a new idea is created
+ idCounter := 0
+
+ // If the .data file does not exist, create it in the cache directory
+ {
+ f, err := os.Open(dataFilename)
+ if errors.Is(err, os.ErrNotExist) {
+ // The file does not exist, so create one with only Version encoded
+ logger.Println("Datafile does not exist. Creating", dataFilename)
+ f, err = os.Create(dataFilename)
+ if err != nil {
+ logger.Fatalln("Error while creating data file:", err)
+ }
+
+ enc := gob.NewEncoder(f)
+ if err := enc.Encode(version); err != nil {
+ logger.Fatalln("Error while encoding Version:", err)
+ }
+ } else if err != nil {
+ logger.Fatalln("Error while opening data file:", err)
+ } else {
+ dec := gob.NewDecoder(f)
+
+ // Check the version
+ var v string
+ if err := dec.Decode(&v); err != io.EOF && err != nil {
+ logger.Fatalln("Error while decoding version:", err)
+ }
+ if v != version {
+ logger.Fatalf("Version mismatch for datafile@%s != package@%s\n", v, version)
+ }
+
+ // Decode the data and import it into Ideas
+ if err := dec.Decode(&name); err != nil && err != io.EOF {
+ logger.Fatalln("Error while decoding name:", err)
+ }
+ if err := dec.Decode(&Ideas); err != nil && err != io.EOF {
+ logger.Fatalln("Error while decoding ideas:", err)
+ }
+ logger.Printf("Imported @%s: %d ideas\n", v, len(Ideas))
+ if err := f.Close(); err != nil {
+ logger.Fatalln("Error while closing file:", err)
+ }
+ }
+
+ }
+
+ // Data was succesfully imported
+ logger.SetPrefix("Ideez(" + name + ") ")
+
+ // Handle SIGINT
+ // Save all Ideas into the data file
+ // NOTE (Luca): This system might seem dumb, but it only adds overhead at startup
+ // and exit of the program which is fine.
+ go func() {
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt)
+ <-c
+
+ f, err := os.Create(dataFilename)
+ if err != nil {
+ logger.Fatalln(err)
+ }
+ defer f.Close()
+
+ enc := gob.NewEncoder(f)
+ if err := enc.Encode(version); err != nil {
+ logger.Fatalln(err)
+ }
+ if err := enc.Encode(name); err != nil {
+ logger.Fatalln(err)
+ }
+ if err := enc.Encode(Ideas); err != nil {
+ logger.Fatalln("Error while saving ideas:", err)
+ }
+
+ logger.Println("data saved.")
+ os.Exit(0)
+ }()
+
+ // Creating templates
+ tmpl, err := template.New("ideas").Parse(ideas_html)
+ if err != nil {
+ logger.Fatalln(err)
+ }
+ _, err = tmpl.New("edit").Parse(idea_edit_html)
+ if err != nil {
+ logger.Fatalln(err)
+ }
+
+ // Register http handlers
+ {
+ mux.HandleFunc("GET "+routePrefix+"/{$}", func(w http.ResponseWriter, r *http.Request) {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "", routePrefix})
+ })
+
+ mux.HandleFunc("POST "+routePrefix+"/idea/create/", func(w http.ResponseWriter, r *http.Request) {
+ i := Idea{
+ Title: r.FormValue("title"),
+ Author: r.FormValue("author"),
+ LastUpdated: time.Now().Format(DateLayout),
+ Text: r.FormValue("text"),
+ Id: idCounter,
+ }
+ if i.Title == "" || i.Author == "" || i.Text == "" {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "All fields are required", routePrefix})
+ return
+ }
+
+ Ideas = append(Ideas, i)
+ idCounter++
+
+ logger.Printf("Added new idea: %#v\n", i)
+ http.Redirect(w, r, routePrefix+"/", http.StatusMovedPermanently)
+ })
+
+ // A page to edit the idea, this page should lead to POST /idea/edit for confirming the edit. If
+ // the user cancels they should be redirected to the start page. This is done in the html.
+ mux.HandleFunc("GET "+routePrefix+"/idea/edit/", func(w http.ResponseWriter, r *http.Request) {
+ id := r.URL.Query().Get("id")
+ if id == "" {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "You must provide an Idea ID.", routePrefix})
+ return
+ }
+ idInt, err := strconv.Atoi(id)
+ if err != nil {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, id + " is not a valid Idea ID.", routePrefix})
+ return
+ }
+
+ for _, i := range Ideas {
+ if i.Id == idInt {
+ tmpl.ExecuteTemplate(w, "edit", struct {
+ Idea Idea
+ RoutePrefix string
+ }{i, routePrefix})
+ return
+ }
+ }
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "No idea with id '" + id + "'.", routePrefix})
+ })
+
+ // Perform the edit action on the idea
+ mux.HandleFunc("POST "+routePrefix+"/idea/edit/", func(w http.ResponseWriter, r *http.Request) {
+ id := r.FormValue("id")
+ if id == "" {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "You must provide an Idea ID.", routePrefix})
+ return
+ }
+
+ idInt, err := strconv.Atoi(id)
+ if err != nil {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, id + " is not a valid Idea ID.", routePrefix})
+ return
+ }
+
+ var i *Idea
+ for j := range Ideas {
+ if Ideas[j].Id == idInt {
+ i = &Ideas[j]
+ break
+ }
+ }
+
+ if i == nil {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "No idea with id '" + id + "'.", routePrefix})
+ return
+ }
+
+ i.Title = r.FormValue("title")
+ i.Text = r.FormValue("text")
+ i.LastUpdated = time.Now().Format(DateLayout) + " (edit)"
+
+ logger.Printf("Edited %#v\n", i)
+
+ http.Redirect(w, r, routePrefix+"/", http.StatusMovedPermanently)
+ })
+
+ mux.HandleFunc("POST "+routePrefix+"/idea/delete/", func(w http.ResponseWriter, r *http.Request) {
+ id := r.FormValue("id")
+ if id == "" {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "You must provide an Idea ID.", routePrefix})
+ return
+ }
+
+ idInt, err := strconv.Atoi(id)
+ if err != nil {
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, id + " is not a valid Idea ID.", routePrefix})
+ return
+ }
+
+ for i, v := range Ideas {
+ if idInt == v.Id {
+ Ideas = append(Ideas[:i], Ideas[i+1:]...)
+ logger.Printf("Deleted: %#v\n", v)
+ http.Redirect(w, r, routePrefix+"/", http.StatusMovedPermanently)
+ return
+ }
+ }
+ tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "No idea with id '" + id + "'.", routePrefix})
+ })
+ }
+
+ return Ideez{Name: name, Id: id}
+}