diff options
author | Raymaekers Luca <raymaekers.luca@gmail.com> | 2024-10-09 16:54:52 +0200 |
---|---|---|
committer | Raymaekers Luca <raymaekers.luca@gmail.com> | 2024-10-12 12:01:51 +0200 |
commit | 2d498443031c1831925d9ba91f8795b7f994677f (patch) | |
tree | 5952a1ae36d41ce20a8257f34c7f9487131203db /server.go | |
parent | 1e0ae41f1ec06379a6602613612c085ad053c0fa (diff) |
Split server code and http code
Create multiple instances of Ideez by running NewIdeez(), after creating
they can be added to the Ideezes slice to manage them. NewIdeez() imports
the data and registers the routes. main.go has the code for managing the
instances and server.go has the code coupled to a single instance. For
eg. main.go has the code which checks files with .data extension in data
directory, and imports servers on startup.
- Add import functionality
- Add nginx configuration
- Add style and copy animation
- Check for https with "X-Forwarded-Proto"
- Change assets and templates to reflect the new id change
- Change port to 15118
- Add Id field to Idea
- Do not check for duplicate titles anymore
- Extract ideez_servers into const variable
- Extract documentation in doc.go
- Rename Server -> Ideez
- Rename t_idea/ -> templates/
- Fix cancel not working with form by using javascript instead
- Set logger prefix in different contexts
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} +} |