package handlers import ( "net/http" "strings" "github.com/dustin/go-humanize" "sneak.berlin/go/simplexcalc/internal/render" ) // widgetListLimit bounds the index query. An unbounded SELECT is fine // on the day it is written and is the outage two years later. const widgetListLimit = 50 // maxWidgetNameLen matches the maxlength on the form input. The form is // a courtesy; this is the rule. const maxWidgetNameLen = 200 // Index renders the front page from the embedded template, listing the // most recent widgets. func (h *Handlers) Index() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() widgets, err := h.params.Database.ListWidgets(ctx, widgetListLimit) if err != nil { h.fail(w, r, http.StatusInternalServerError, "Could not load widgets.", err) return } count, err := h.params.Database.CountWidgets(ctx) if err != nil { h.fail(w, r, http.StatusInternalServerError, "Could not count widgets.", err) return } data := render.IndexPage{ Page: h.page(r), WidgetCount: count, Widgets: widgets, } err = h.params.Renderer.HTML(w, http.StatusOK, "index.html", data) if err != nil { h.fail(w, r, http.StatusInternalServerError, "Could not render the page.", err) } } } // CreateWidget handles the form POST. State-changing, so it is behind // CSRF; see internal/server/routes.go for where that is applied. func (h *Handlers) CreateWidget() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // ParseForm reads the body, which BodyLimit has already capped: // an oversized submission fails here rather than being buffered // in full first. err := r.ParseForm() if err != nil { h.fail(w, r, http.StatusBadRequest, "Could not read the form.", err) return } name := strings.TrimSpace(r.PostFormValue("name")) if name == "" || len(name) > maxWidgetNameLen { h.fail(w, r, http.StatusBadRequest, "A widget needs a name of 1 to 200 characters.", errBadWidgetName) return } size, err := parseSize(r.PostFormValue("size")) if err != nil { h.fail(w, r, http.StatusBadRequest, "Size must be a byte count, like 4096 or 4KiB.", err) return } _, err = h.params.Database.CreateWidget(r.Context(), name, size) if err != nil { h.fail(w, r, http.StatusInternalServerError, "Could not save the widget.", err) return } // POST/redirect/GET: a reload must not repeat the write. http.Redirect(w, r, "/", http.StatusSeeOther) } } // parseSize accepts an empty value as zero and anything else as a // human-readable byte size. func parseSize(s string) (int64, error) { s = strings.TrimSpace(s) if s == "" { return 0, nil } n, err := humanize.ParseBytes(s) if err != nil { return 0, errBadWidgetSize } // A size beyond this is not a widget, it is a typo with a suffix. const maxWidgetSize = uint64(1) << 50 if n > maxWidgetSize { return 0, errBadWidgetSize } return int64(n), nil }