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)
}