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 writes the 400 itself and reports false, so callers can just return.
14func parseID(w http.ResponseWriter, r *http.Request) (int64, bool) {
15 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
16 if err != nil {
17 http.Error(w, "Invalid ID", http.StatusBadRequest)
18 return 0, false
19 }
20 return id, true
21}
22
23// sortFromRequest remembers an explicit ?sort= in a cookie and restores it
24// otherwise, so the order survives the auto-refresh that reloads these pages.
25func sortFromRequest(w http.ResponseWriter, r *http.Request, cookie, defaultSort string) string {
26 sortBy := r.URL.Query().Get("sort")
27 if sortBy == "" {
28 return getCookie(r, cookie, defaultSort)
29 }
30 setCookie(w, cookie, sortBy)
31 return sortBy
32}
33
34// pageSize: the library page fires one thumbnail request per media file, so an
35// unpaginated large archive is both a slow render and a request storm.
36const pageSize = 50
37
38// Query holds the request's other parameters (filter, sort, path) with a
39// trailing '&', so a link keeps them. It is template.URL because Encode already
40// percent-encoded it: as a plain string the template escaper would treat the
41// whole query as one value and re-encode its '&' and '='.
42type Pagination struct {
43 Page int
44 Prev int
45 Next int
46 HasNext bool
47 Query template.URL
48}
49
50// pageFromRequest reads the 1-based ?page=. Unparseable, below 1, or large
51// enough to overflow page*pageSize all mean page 1: a hand-edited URL must not
52// produce a negative offset.
53func pageFromRequest(r *http.Request) int {
54 p, err := strconv.Atoi(r.URL.Query().Get("page"))
55 if err != nil || p < 1 || p > math.MaxInt/pageSize {
56 return 1
57 }
58 return p
59}
60
61// hasNext comes from the caller: each listing determines it differently.
62func newPagination(r *http.Request, page int, hasNext bool) Pagination {
63 q := r.URL.Query()
64 q.Del("page")
65 query := q.Encode()
66 if query != "" {
67 query += "&"
68 }
69 return Pagination{
70 Page: page,
71 Prev: page - 1,
72 Next: page + 1,
73 HasNext: hasNext,
74 Query: template.URL(query),
75 }
76}
77
78// serverError keeps the error out of the response: internal error strings can
79// carry filesystem paths and SQL text.
80func (h *Handler) serverError(w http.ResponseWriter, r *http.Request, context string, err error) {
81 slog.Error(context, "method", r.Method, "path", r.URL.Path, "err", err)
82 http.Error(w, "Something went wrong. Please try again.", http.StatusInternalServerError)
83}
84
85// redirectWithError is the Post/Redirect/Get failure path for forms. The
86// underlying error is logged rather than shown.
87func redirectWithError(w http.ResponseWriter, r *http.Request, path, message string, err error) {
88 if err != nil {
89 slog.Error(message, "method", r.Method, "path", r.URL.Path, "err", err)
90 }
91 flashError(w, message)
92 http.Redirect(w, r, path, http.StatusSeeOther)
93}
94
95func redirectWithSuccess(w http.ResponseWriter, r *http.Request, path, message string) {
96 flashSuccess(w, message)
97 http.Redirect(w, r, path, http.StatusSeeOther)
98}
99
100// parseForm writes a generic 400 itself: ParseForm's own error names the
101// offending bytes, which is noise to the user and detail not worth handing out.
102func parseForm(w http.ResponseWriter, r *http.Request) bool {
103 if err := r.ParseForm(); err != nil {
104 http.Error(w, "Invalid form", http.StatusBadRequest)
105 return false
106 }
107 return true
108}
109