helpers.go
⎇
Raw
1package handler
2
3import (
4 "html/template"
5 "log/slog"
6 "math"
7 "net/http"
8 "strconv"
9
10 "github.com/go-chi/chi/v5"
11)
12
13// parseID reads the {id} route parameter. It writes a 400 and reports false when
14// the value isn't a valid id, so callers can simply return.
15func parseID(w http.ResponseWriter, r *http.Request) (int64, bool) {
16 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
17 if err != nil {
18 http.Error(w, "Invalid ID", http.StatusBadRequest)
19 return 0, false
20 }
21 return id, true
22}
23
24// sortFromRequest returns the sort order for a listing page. An explicit ?sort=
25// is remembered in a cookie; without one the last choice is restored, so the
26// order survives the auto-refresh that reloads these pages.
27func sortFromRequest(w http.ResponseWriter, r *http.Request, cookie, defaultSort string) string {
28 sortBy := r.URL.Query().Get("sort")
29 if sortBy == "" {
30 return getCookie(r, cookie, defaultSort)
31 }
32 setCookie(w, cookie, sortBy)
33 return sortBy
34}
35
36// pageSize is how many items a listing page renders. The library page fires one
37// thumbnail request per media file, so an unpaginated page of a large archive is
38// both a slow render and a request storm.
39const pageSize = 50
40
41// Pagination carries what a listing template needs for its prev/next links.
42// Query holds the request's other parameters (filter, sort, path) with a
43// trailing '&', so a link keeps them. It is template.URL because
44// url.Values.Encode already percent-encoded it: as a plain string the template
45// escaper would treat the whole query as one value and re-encode its '&' and
46// '='.
47type Pagination struct {
48 Page int
49 Prev int
50 Next int
51 HasNext bool
52 Query template.URL
53}
54
55// pageFromRequest reads the 1-based ?page=. Anything unparseable, below 1, or
56// large enough that page*pageSize would overflow is page 1: a hand-edited URL
57// must not produce a negative offset, which slices a listing out of range.
58func pageFromRequest(r *http.Request) int {
59 p, err := strconv.Atoi(r.URL.Query().Get("page"))
60 if err != nil || p < 1 || p > math.MaxInt/pageSize {
61 return 1
62 }
63 return p
64}
65
66// newPagination describes the page links for a request. hasNext comes from the
67// caller because each listing determines it differently.
68func newPagination(r *http.Request, page int, hasNext bool) Pagination {
69 q := r.URL.Query()
70 q.Del("page")
71 query := q.Encode()
72 if query != "" {
73 query += "&"
74 }
75 return Pagination{
76 Page: page,
77 Prev: page - 1,
78 Next: page + 1,
79 HasNext: hasNext,
80 Query: template.URL(query),
81 }
82}
83
84// serverError logs the underlying failure and shows the user a generic message.
85// Internal error strings can carry filesystem paths and SQL text, so they are
86// kept out of the response.
87func (h *Handler) serverError(w http.ResponseWriter, r *http.Request, context string, err error) {
88 slog.Error(context, "method", r.Method, "path", r.URL.Path, "err", err)
89 http.Error(w, "Something went wrong. Please try again.", http.StatusInternalServerError)
90}
91
92// redirectWithError flashes a message and redirects, the standard
93// Post/Redirect/Get failure path for form submissions. The underlying error is
94// logged rather than shown.
95func redirectWithError(w http.ResponseWriter, r *http.Request, path, message string, err error) {
96 if err != nil {
97 slog.Error(message, "method", r.Method, "path", r.URL.Path, "err", err)
98 }
99 flashError(w, message)
100 http.Redirect(w, r, path, http.StatusSeeOther)
101}
102
103// redirectWithSuccess flashes a confirmation and redirects.
104func redirectWithSuccess(w http.ResponseWriter, r *http.Request, path, message string) {
105 flashSuccess(w, message)
106 http.Redirect(w, r, path, http.StatusSeeOther)
107}
108
109// parseForm parses a form body and reports whether it succeeded. On failure it
110// writes a 400 with a generic message: ParseForm's own error names the offending
111// bytes, which is noise to the user and detail we don't need to hand out.
112func parseForm(w http.ResponseWriter, r *http.Request) bool {
113 if err := r.ParseForm(); err != nil {
114 http.Error(w, "Invalid form", http.StatusBadRequest)
115 return false
116 }
117 return true
118}
119