diff options
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) } } |