aboutsummaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorRaymaekers Luca <raymaekers.luca@gmail.com>2024-10-09 16:54:52 +0200
committerRaymaekers Luca <raymaekers.luca@gmail.com>2024-10-12 12:01:51 +0200
commit2d498443031c1831925d9ba91f8795b7f994677f (patch)
tree5952a1ae36d41ce20a8257f34c7f9487131203db /main.go
parent1e0ae41f1ec06379a6602613612c085ad053c0fa (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.go308
1 files changed, 81 insertions, 227 deletions
diff --git a/main.go b/main.go
index d4c2c15..ddc8728 100644
--- a/main.go
+++ b/main.go
@@ -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)
}
}