package handler import ( "html/template" "log/slog" "math" "net/http" "strconv" "github.com/go-chi/chi/v5" ) // parseID reads the {id} route parameter. It writes a 400 and reports false when // the value isn't a valid id, so callers can simply return. func parseID(w http.ResponseWriter, r *http.Request) (int64, bool) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return 0, false } return id, true } // sortFromRequest returns the sort order for a listing page. An explicit ?sort= // is remembered in a cookie; without one the last choice is restored, so the // order survives the auto-refresh that reloads these pages. func sortFromRequest(w http.ResponseWriter, r *http.Request, cookie, defaultSort string) string { sortBy := r.URL.Query().Get("sort") if sortBy == "" { return getCookie(r, cookie, defaultSort) } setCookie(w, cookie, sortBy) return sortBy } // pageSize is how many items a listing page renders. The library page fires one // thumbnail request per media file, so an unpaginated page of a large archive is // both a slow render and a request storm. const pageSize = 50 // Pagination carries what a listing template needs for its prev/next links. // Query holds the request's other parameters (filter, sort, path) with a // trailing '&', so a link keeps them. It is template.URL because // url.Values.Encode already percent-encoded it: as a plain string the template // escaper would treat the whole query as one value and re-encode its '&' and // '='. type Pagination struct { Page int Prev int Next int HasNext bool Query template.URL } // pageFromRequest reads the 1-based ?page=. Anything unparseable, below 1, or // large enough that page*pageSize would overflow is page 1: a hand-edited URL // must not produce a negative offset, which slices a listing out of range. func pageFromRequest(r *http.Request) int { p, err := strconv.Atoi(r.URL.Query().Get("page")) if err != nil || p < 1 || p > math.MaxInt/pageSize { return 1 } return p } // newPagination describes the page links for a request. hasNext comes from the // caller because each listing determines it differently. func newPagination(r *http.Request, page int, hasNext bool) Pagination { q := r.URL.Query() q.Del("page") query := q.Encode() if query != "" { query += "&" } return Pagination{ Page: page, Prev: page - 1, Next: page + 1, HasNext: hasNext, Query: template.URL(query), } } // serverError logs the underlying failure and shows the user a generic message. // Internal error strings can carry filesystem paths and SQL text, so they are // kept out of the response. func (h *Handler) serverError(w http.ResponseWriter, r *http.Request, context string, err error) { slog.Error(context, "method", r.Method, "path", r.URL.Path, "err", err) http.Error(w, "Something went wrong. Please try again.", http.StatusInternalServerError) } // redirectWithError flashes a message and redirects, the standard // Post/Redirect/Get failure path for form submissions. The underlying error is // logged rather than shown. func redirectWithError(w http.ResponseWriter, r *http.Request, path, message string, err error) { if err != nil { slog.Error(message, "method", r.Method, "path", r.URL.Path, "err", err) } flashError(w, message) http.Redirect(w, r, path, http.StatusSeeOther) } // redirectWithSuccess flashes a confirmation and redirects. func redirectWithSuccess(w http.ResponseWriter, r *http.Request, path, message string) { flashSuccess(w, message) http.Redirect(w, r, path, http.StatusSeeOther) }