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 /main.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 'main.go')
-rw-r--r-- | main.go | 308 |
1 files changed, 81 insertions, 227 deletions
@@ -1,279 +1,133 @@ -package main - // ideez is a web application that allows people to post their ideas, it is meant to help // brainstorming. - -// ToDo's -// - [ ] Create a Server out of this so you can run multiple instances -// - [ ] change CreatedAt to be Last Updated -// - [x] edit a post -// - [x] Store ideas to a file (encoder/gob) -// - [x] Change the date format printing -// - [x] outsource removing the posts to a separate cli tool -// - [x] Add a post -// - [x] Remove a post +package main import ( - _ "embed" - "encoding/gob" + "crypto/rand" + "encoding/hex" "errors" - "fmt" "html/template" - "io" "log" "net/http" "os" - "os/signal" - "runtime/debug" - "time" -) - -// File for persistent storage -var DataFilename = "ideas.data" - -// Version number used for debugging compatibility with the .data file -var Version string + "strings" -// layout for how the date should be output in html -var DateLayout string = "02/01/2006 on 15:04" - -// template for ideas html -var ( - //go:embed t_idea/index.html - ideas_html string - //go:embed t_idea/edit.html - idea_edit_html string + _ "embed" ) -var Ideas []Idea - -// Represents an idea -// LastUpdated is a formatted date string -// out of 5 rating of the idea -// formatted time string with DateLayout -type Idea struct { - Title string - Text string - Author string - LastUpdated string -} - -// Data passed to the ideas_html template -type PageData struct { - Ideas []Idea - Error string +type ServersPageData struct { + Ideezes []Ideez + Address template.URL + Error string } -func GetVersion() string { - buildinfo, ok := debug.ReadBuildInfo() - if !ok { - log.Fatalln("Could not read buildinfo to know package version.") - } - return buildinfo.Main.Version -} - -func main() { - Version = GetVersion() - - // If the .data file does not exist, create it in the cache directory - { - p := os.Getenv("XDG_CACHE_HOME") - if p == "" { - p = "." - } - DataFilename = p + "/" + DataFilename - - f, err := os.Open(DataFilename) - if errors.Is(err, os.ErrNotExist) { - // The file does not exist, so create one with only Version encoded - log.Println("File does not exist, creating", DataFilename) - f, err = os.Create(DataFilename) - if err != nil { - log.Fatalln("Error while creating data file:", err) - } - - enc := gob.NewEncoder(f) - if err := enc.Encode(Version); err != nil { - log.Fatalln("Error while encoding Version:", err) - } - } else if err != nil { - log.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 { - log.Fatalln("Error while decoding version:", err) - } - if v != Version { - log.Fatalf("Version mismatch for datafile@%s != package@%s\n", v, Version) - } - - // Decode the data and import it into Ideas - err = dec.Decode(&Ideas) - if err != nil && err != io.EOF { - log.Fatalln("Error while decoding ideas:", err) - } - log.Printf("Imported @%s: %d ideas\n", v, len(Ideas)) - if err := f.Close(); err != nil { - log.Fatalln("Error while closing file:", err) - } - } - - } - - // 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 { - log.Fatalln(err) - } - defer f.Close() +const ( + // Name of the directory where the data files are stored + DATA_DIR = "ideez_servers" +) - enc := gob.NewEncoder(f) - if err := enc.Encode(Version); err != nil { - log.Fatalln(err) - } +var address string = "localhost:15118" - if err := enc.Encode(Ideas); err != nil { - log.Fatalln("Error while saving ideas:", err) - } +//go:embed templates/servers.html +var servers_html string - log.Println("data saved.") - os.Exit(0) - }() +// All servers that are currently managed +var Ideezes []Ideez - // Handling http +// Directory path where the data files are stored +var DataDir string - tmpl, err := template.New("ideas_html").Parse(ideas_html) - if err != nil { - log.Fatalln(err) +func getAddress(r *http.Request) template.URL { + p, ok := r.Header["X-Forwarded-Proto"] + if ok { + return template.URL(p[0] + "://" + r.Host) } - _, err = tmpl.New("edit").Parse(idea_edit_html) - if err != nil { - log.Fatalln(err) + if r.TLS == nil { + return template.URL("http://" + r.Host) + } else { + return template.URL("https://" + r.Host) } +} - // TODO (Luca): Make the app more interactive by using websockets instead, - // such that adding or editing does not require to - // refresh the page. - // Another approach would be to use htmx? - // +func main() { + var ( + logger = log.New(os.Stdout, "Ideez: ", log.Ldate) + ) mux := http.NewServeMux() fs := http.FileServer(http.Dir("assets/")) mux.Handle("/static/", http.StripPrefix("/static/", fs)) - mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) - }) + tmpl, err := template.New("servers").Parse(servers_html) - mux.HandleFunc("GET /ideas/", func(w http.ResponseWriter, r *http.Request) { - tmpl.Execute(w, PageData{Ideas, ""}) + // list servers + mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { + tmpl.ExecuteTemplate(w, "servers", ServersPageData{Ideezes, getAddress(r), ""}) }) - mux.HandleFunc("POST /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"), - } - if i.Title == "" || i.Author == "" || i.Text == "" { - tmpl.Execute(w, PageData{Ideas, "All fields are required"}) + // create a new server + mux.HandleFunc("POST /server/create/", func(w http.ResponseWriter, r *http.Request) { + addr := getAddress(r) + name := r.FormValue("name") + logger.Println("Create server", name) + if name == "" { + tmpl.ExecuteTemplate(w, "servers", ServersPageData{Ideezes, addr, "You must provide a title"}) return } - for _, v := range Ideas { - if i.Title == v.Title { - tmpl.Execute(w, PageData{Ideas, "An idea with title '" + v.Title + "' already exists!"}) + for _, v := range Ideezes { + if v.Name == name { + tmpl.ExecuteTemplate(w, "servers", ServersPageData{Ideezes, addr, "A server with name '" + name + "' already exists!"}) return } } - Ideas = append(Ideas, i) - log.Println("Added new idea:", i.Title) - http.Redirect(w, r, "/ideas/", 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 /idea/edit/", func(w http.ResponseWriter, r *http.Request) { - t := r.URL.Query().Get("t") - if t == "" { - tmpl.Execute(w, PageData{Ideas, "You must provide a title."}) - return + bytes := make([]byte, SERVER_ID_BYTES) + _, err := rand.Read(bytes) + if err != nil { + panic(err) } + id := hex.EncodeToString(bytes) - for _, i := range Ideas { - if i.Title == t { - tmpl.ExecuteTemplate(w, "edit", i) - return - } - } - tmpl.Execute(w, PageData{Ideas, "No idea with title '" + t + "'."}) + server := NewIdeez(mux, name, id) + Ideezes = append(Ideezes, server) + http.Redirect(w, r, "/", http.StatusMovedPermanently) }) - // Perform the edit action on the idea - mux.HandleFunc("POST /idea/edit/", func(w http.ResponseWriter, r *http.Request) { - t := r.FormValue("title") - if t == "" { - tmpl.Execute(w, PageData{Ideas, "You must provide a title."}) - return - } - - var i *Idea - for j := range Ideas { - if Ideas[j].Title == t { - i = &Ideas[j] - break - } + // import the servers from the data files + { + p := os.Getenv("XDG_CACHE_HOME") + if p == "" { + p = "." } - if i.Title == "" { - tmpl.Execute(w, PageData{Ideas, "No idea with title '" + t + "'."}) + DataDir = p + "/" + DATA_DIR + if err := os.Mkdir(DataDir, 0755); err == nil { + logger.Println("Directory does not exist, creating " + DataDir) + } else if !errors.Is(err, os.ErrExist) { + panic(err) } - i.Title = r.FormValue("title") - i.Text = r.FormValue("text") - i.LastUpdated = time.Now().Format(DateLayout) + " (edit)" - log.Printf("Edited '%s'\n", i.Title) - - http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) - }) - - mux.HandleFunc("POST /idea/delete/", func(w http.ResponseWriter, r *http.Request) { - t := r.FormValue("title") - if t == "" { - tmpl.Execute(w, PageData{Ideas, "You must provide a title."}) - return + entries, err := os.ReadDir(DataDir) + if err != nil { + panic(err) } - for i, v := range Ideas { - if t == v.Title { - log.Println("Deleted:", v.Title) - Ideas = append(Ideas[:i], Ideas[i+1:]...) - http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) - return + counter := 0 + for _, file := range entries { + if !strings.HasSuffix(file.Name(), ".data") { + continue } + id := strings.TrimSuffix(file.Name(), ".data") + ideez := NewIdeez(mux, "", id) + Ideezes = append(Ideezes, ideez) + counter++ } - tmpl.Execute(w, PageData{Ideas, "No idea with title '" + t + "'."}) - }) - - mux.HandleFunc("POST /comment/create/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Not implemented yet.") - }) + logger.Printf("Imported %d Ideezes\n", counter) + } - log.Println("Listening on http://localhost:8080/ideas") - err = http.ListenAndServe(":8080", mux) + logger.Println("Listening on http://" + address) + err = http.ListenAndServe(address, mux) if err != nil { - log.Fatalln(err) + panic(err) } } |