package handler
import (
"html/template"
"log/slog"
"math"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
// parseID writes the 400 itself and reports false, so callers can just 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 remembers an explicit ?sort= in a cookie and restores it
// otherwise, 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: the library page fires one thumbnail request per media file, so an
// unpaginated large archive is both a slow render and a request storm.
const pageSize = 50
// Query holds the request's other parameters (filter, sort, path) with a
// trailing '&', so a link keeps them. It is template.URL because 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=. Unparseable, below 1, or large
// enough to overflow page*pageSize all mean page 1: a hand-edited URL must not
// produce a negative offset.
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
}
// hasNext comes from the caller: 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 keeps the error out of the response: internal error strings can
// carry filesystem paths and SQL text.
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 is the Post/Redirect/Get failure path for forms. 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)
}
func redirectWithSuccess(w http.ResponseWriter, r *http.Request, path, message string) {
flashSuccess(w, message)
http.Redirect(w, r, path, http.StatusSeeOther)
}
// parseForm writes a generic 400 itself: ParseForm's own error names the
// offending bytes, which is noise to the user and detail not worth handing out.
func parseForm(w http.ResponseWriter, r *http.Request) bool {
if err := r.ParseForm(); err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
return false
}
return true
}