package main import ( "bytes" "encoding/json" "errors" "fmt" "html/template" "net/http" "os" "slices" "strconv" "strings" "sync" "unicode/utf8" ) const todoFile = "todo.json" const maxNameLength = 100 type task struct { Name string } // errNoTask means that no task has the index asked for. var errNoTask = errors.New("no task has that index") // mu protects tasks, and the file that holds it. var mu sync.Mutex var tasks = []task{} func loadTasks() ([]task, error) { data, err := os.ReadFile(todoFile) if errors.Is(err, os.ErrNotExist) { return []task{}, nil } if err != nil { return nil, err } var loaded []task err = json.Unmarshal(data, &loaded) if err != nil { return nil, fmt.Errorf("%s is not valid: %v", todoFile, err) } return loaded, nil } func saveTasks(list []task) error { data, err := json.MarshalIndent(list, "", " ") if err != nil { return err } temporary := todoFile + ".tmp" err = os.WriteFile(temporary, data, 0644) if err != nil { return err } return os.Rename(temporary, todoFile) } func allTasks() []task { mu.Lock() defer mu.Unlock() return slices.Clone(tasks) } func addTask(t task) error { mu.Lock() defer mu.Unlock() updated := append(slices.Clone(tasks), t) err := saveTasks(updated) if err != nil { return err } tasks = updated return nil } // removeTask removes the task at index n, counting from 0. The check is inside // the lock, because another request could change the list at any moment. func removeTask(n int) error { mu.Lock() defer mu.Unlock() if n < 0 || n >= len(tasks) { return errNoTask } updated := slices.Clone(tasks) updated = append(updated[:n], updated[n+1:]...) err := saveTasks(updated) if err != nil { return err } tasks = updated return nil } const todoText = ` To-do

To-do

` var todoTemplate = template.Must(template.New("todo").Parse(todoText)) func handleHome(w http.ResponseWriter, r *http.Request) { var page bytes.Buffer err := todoTemplate.Execute(&page, allTasks()) if err != nil { fmt.Println("Template error:", err) http.Error(w, "The page could not be built.", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") page.WriteTo(w) } func handleAdd(w http.ResponseWriter, r *http.Request) { name := strings.TrimSpace(r.FormValue("name")) if name == "" { http.Error(w, "The task has no name.", http.StatusBadRequest) return } if utf8.RuneCountInString(name) > maxNameLength { http.Error(w, "The name is too long.", http.StatusBadRequest) return } err := addTask(task{Name: name}) if err != nil { fmt.Println("Save error:", err) http.Error(w, "The task could not be saved.", http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusSeeOther) } func handleDelete(w http.ResponseWriter, r *http.Request) { n, err := strconv.Atoi(r.PathValue("n")) if err != nil { http.Error(w, "The index must be a whole number.", http.StatusBadRequest) return } err = removeTask(n) if errors.Is(err, errNoTask) { http.Error(w, err.Error(), http.StatusNotFound) return } if err != nil { fmt.Println("Save error:", err) http.Error(w, "The list could not be saved.", http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusSeeOther) } func main() { loaded, err := loadTasks() if err != nil { fmt.Println("Could not load the list:", err) fmt.Println("Fix or remove the file, then start the server again.") return } tasks = loaded http.HandleFunc("GET /{$}", handleHome) http.HandleFunc("POST /tasks", handleAdd) http.HandleFunc("POST /delete/{n}", handleDelete) fmt.Println("Server running at http://localhost:8080") fmt.Println("Press Ctrl+C to stop it.") err = http.ListenAndServe("localhost:8080", nil) if err != nil { fmt.Println("Server stopped:", err) } }