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} }