diff options
Diffstat (limited to 'server.go')
-rw-r--r-- | server.go | 289 |
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} +} |