Documentation
¶
Overview ¶
Package gallery implements an image-gallery HTTP handler for Caddy v2. It renders a directory as a dark-themed grid of thumbnails with a vanilla JS lightbox for click-to-expand.
Index ¶
- func GenerateOrLoadThumb(src, cacheDir string, cfg ThumbConfig) ([]byte, error)
- func GenerateOrLoadVideoThumb(src, cacheDir, ffmpegPath string, cfg ThumbConfig) ([]byte, error)
- func RenderPage(title, pathPrefix, thumbPrefix, relPath, tmplName string, ...) (string, error)
- func ThumbPath(src, cacheDir string) string
- type FileInfo
- type FileKind
- type FileView
- type Gallery
- func (Gallery) CaddyModule() caddy.ModuleInfo
- func (*Gallery) Cleanup()
- func (g *Gallery) Provision(caddy.Context) error
- func (g *Gallery) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error
- func (g *Gallery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error
- func (*Gallery) Validate() error
- type PageData
- type ScanCache
- type Scanner
- type SortSpec
- type ThumbConfig
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GenerateOrLoadThumb ¶
func GenerateOrLoadThumb(src, cacheDir string, cfg ThumbConfig) ([]byte, error)
GenerateOrLoadThumb returns the thumbnail bytes for src, generating and caching on first call and serving from cache on subsequent calls. The cfg parameter (Width, Height, Format) controls the output size and output format. The source is fit-within-bounds (aspect ratio preserved, longest edge becomes the configured value). Sources already within the box are encoded without resizing.
The cache file is at <cacheDir>/<sha256(absolute source path)>.<ext> keyed by (source path, cfg.Format) so changing the format invalidates the old cache automatically. The cache is also regenerated when the source mtime is newer than the cache mtime.
func GenerateOrLoadVideoThumb ¶
func GenerateOrLoadVideoThumb(src, cacheDir, ffmpegPath string, cfg ThumbConfig) ([]byte, error)
GenerateOrLoadVideoThumb extracts the first frame from a video using ffmpeg, saves it as a WebP thumbnail, and returns the thumbnail bytes. Mirrors GenerateOrLoadThumb's caching behavior: if a cache file already exists and is not older than the source video, the cached bytes are returned without invoking ffmpeg.
The ffmpegPath argument must be the absolute path to a working ffmpeg binary (use Gallery.ffmpegPath, set in Provision via exec.LookPath). If ffmpegPath is empty, the function returns an error — callers should check gallery.VideoThumbsEnabled() first to know whether to call this function.
ffmpeg invocation:
ffmpeg -y -i input.mp4 -vframes 1 -vf "scale=W:H:force_original_aspect_ratio=decrease" output.webp
-y: overwrite output without prompting -i: input -vframes 1: extract exactly one frame -vf scale=W:H:force_original_aspect_ratio=decrease: fit-within-bounds output: webp (or other format if cfg.Format is set; we default to webp since that's what the rest of the thumb pipeline uses)
Note on seeking: we use -vframes 1 which extracts the FIRST frame. For most videos this is fine. Some videos have an all- black opening frame (the "fade-in" frame); if that becomes a problem we can add -ss 0.5 to seek forward half a second.
func RenderPage ¶
func RenderPage(title, pathPrefix, thumbPrefix, relPath, tmplName string, noThumbs, noVideoThumbs bool, pageSize int, files []FileInfo, query url.Values) (string, error)
RenderPage renders the gallery page for a directory. The caller provides the raw directory listing (output of Scanner.Scan); RenderPage does the split / sort / paginate / format work.
`title` is the page heading (e.g. "Generated Images"). It is derived by the caller — typically the basename of the current directory.
`pathPrefix` and `thumbPrefix` are the URL prefixes for relative links. The defaults used in the live config are "./" and "./_thumbs/" — both relative so they work for any subdir the gallery is mounted at.
`relPath` is the path within the gallery (the request's post-handle_path-stripped path, no leading slash). Empty for the gallery root. When non-empty, an ".." entry is prepended to the directories list so the user can navigate up.
`query` is the request's URL query values; sort and page are read from it. RenderPage renders the gallery. `tmplName` is the configured template name (relative to the templates dir). Pass "" to use the default ("gallery.tmpl"). The name is validated inside loadTemplate. RenderPage renders the gallery. `tmplName` is the configured template name (relative to the templates dir). Pass "" to use the default ("gallery.tmpl"). `noThumbs` is the configured no_thumbs flag — when true, image tiles use the original file as the <img src> instead of `/_thumbs/<name>.webp` (no thumb generation). `pageSize` is the configured page_size — the number of image entries per page. Pass 0 for the default of 50.
func ThumbPath ¶
ThumbPath returns the on-disk path where the thumbnail for src should be cached. The filename is the first 16 bytes of the SHA256 of src's absolute path, hex-encoded (32 hex chars) + ".webp". Using a content-hash means cache entries are stable across renames of the parent directory (as long as the absolute source path stays the same) and collisions are effectively impossible.
Types ¶
type FileInfo ¶
type FileInfo struct {
Name string `json:"name"`
ModTime int64 `json:"mtime"`
Size int64 `json:"size"`
Kind FileKind `json:"kind"`
}
FileInfo describes a single entry in a gallery directory. This is the type that flows into the template renderer.
ModTime is unix nanoseconds. int64 keeps the type JSON-friendly (no time.Time marshalling quirks) and nanosecond resolution preserves sub-second ordering of files written close together.
type FileKind ¶
type FileKind string
FileKind categorises an entry in a gallery directory.
const ( // KindDir is a subdirectory. The Name is the directory basename // (not the full path); the scanner joins it to Root at render // time. KindDir FileKind = "dir" // KindImage is an image file (jpg, png, gif, webp, svg, avif). KindImage FileKind = "image" // KindVideo is a video file (mp4, webm). KindVideo FileKind = "video" // KindOther is a non-image, non-video file (html, txt, etc.). // These are shown in the "Other files" strip above the image // grid. KindOther FileKind = "other" )
type FileView ¶
type FileView struct {
Name string // basename ("photo.jpg" or "subdir")
Href string // relative link
ThumbURL string // for images, the relative thumb URL; empty for non-images
IsDir bool
IsUp bool // true for the synthetic "../" up-link entry (rendered with ↑ icon, no trailing /)
IsImage bool
IsVideo bool
IsOther bool
// ParentDir is set ONLY on the up-link FileView. It is the
// basename of the parent directory (one level up from the
// current page). Rendered as part of the up chip's display
// text — "Up (../{ParentDir})" — so the user sees which
// directory they'll land in. Empty at the gallery root or
// in a top-level subdir (parent is the gallery root, no name
// to show). Per user request 2026-06-17.
ParentDir string
// Internal fields used by buildFileView to format Size/Date
// without round-tripping through fmt in the template.
Size string
Date string
Type string
}
FileView is the template-friendly representation of a single entry. All display strings are pre-formatted (no template computation needed). Href and ThumbURL are relative to the current page.
type Gallery ¶
type Gallery struct {
// Root is the on-disk directory to render. Set automatically by
// Caddy's `root` directive (via Provision), or can be set in JSON
// config.
Root string `json:"root,omitempty"`
// Sort is the field used to order the gallery. Valid values:
// "mtime" (default) — newest first
// "name" — alphabetical
Sort string `json:"sort,omitempty"`
// Template is the name of the template file to use, relative to
// the templates dir ($GALLERY_TEMPLATES_DIR, default
// /etc/caddy/gallery-templates). If empty, defaults to
// "gallery.tmpl". The path is validated at Provision to
// reject absolute paths and any traversal above the templates
// dir (no `..` allowed). The template dir is the chroot; the
// operator can only reference files inside it.
Template string `json:"template,omitempty"`
// NoThumbs disables the on-the-fly WebP thumbnail generation.
// When true, the gallery uses the original image as the tile
// <img src> instead of `/_thumbs/<name>.webp`. Requests to the
// thumb URL fall through to the next handler (typically
// file_server, which 404s since no _thumbs/ dir exists).
// Tradeoffs: no thumb cache, no CPU cost, but the browser
// downloads the full image per tile (bigger page payload, slower
// load on dirs of large photos). Useful for small galleries
// where the operator doesn't want to maintain a thumb cache.
NoThumbs bool `json:"no_thumbs,omitempty"`
// NoVideoThumbs disables the on-demand WebP thumbnail generation
// for VIDEO files (extracted from the first frame via ffmpeg).
// When true, videos still display in the gallery (with the
// placeholder gradient + play button on each tile) but no
// per-frame thumbnail is produced or served. When false (the
// default), video thumbnails ARE generated IF ffmpeg is
// available on the host — if ffmpeg is missing, video thumbs
// fall back to the placeholder regardless of this setting
// (there's no way to generate a frame without a tool that can
// decode the video).
// Caddyfile: `no_video_thumbs` (no arg → true) or
// `no_video_thumbs false` (re-enable).
NoVideoThumbs bool `json:"no_video_thumbs,omitempty"`
// PageSize is the number of image entries per page. Default
// is 50 (set in Provision if zero). The user can override
// per-route via the Caddyfile: `image_gallery { page_size 100 }`.
// Validation: must be > 0. A zero or negative value is rejected
// by UnmarshalCaddyfile (the Caddyfile parser).
PageSize int `json:"page_size,omitempty"`
// ThumbWidth is the maximum width in pixels of generated
// thumbnails. The source image is fit-within-bounds (aspect
// ratio preserved, longest edge becomes the configured value).
// Default: 320. Caddyfile: `thumb_width 480`.
// Validation: must be > 0; zero/negative rejected.
ThumbWidth int `json:"thumb_width,omitempty"`
// ThumbHeight is the maximum height in pixels of generated
// thumbnails. Fit-within-bounds behavior — see ThumbWidth.
// Default: 320. Caddyfile: `thumb_height 480`.
// Validation: must be > 0.
ThumbHeight int `json:"thumb_height,omitempty"`
// ThumbFormat is the output format for generated thumbnails.
// One of: "jpeg" (or "jpg"), "png", or "webp" (the current
// default, lossless). Default: "webp". Caddyfile:
// `thumb_format jpeg`. Validation: must be one of the three.
ThumbFormat string `json:"thumb_format,omitempty"`
// CacheScanMinutes is the in-memory scan cache TTL in
// minutes. Default: 1. Caddyfile: `cache_scan 5`.
// Validation: must be > 0.
CacheScanMinutes int `json:"cache_scan,omitempty"`
// ThumbTTLMinutes is the HTTP Cache-Control max-age in
// minutes for thumb responses. Thumbs are immutable per
// source mtime, so a long TTL is safe. Default: 1440
// (= 24 hours, matches the previous 86400-second value).
// Caddyfile: `thumb_ttl 60`. Validation: must be > 0.
ThumbTTLMinutes int `json:"thumb_ttl,omitempty"`
// Cache holds the in-memory scan cache. Initialised in Provision
// if nil. Excluded from JSON config (runtime state only).
Cache *ScanCache `json:"-"`
// contains filtered or unexported fields
}
Gallery is a Caddy HTTP handler that renders a directory as a dark-themed image/video gallery. See the README for behaviour.
func (Gallery) CaddyModule ¶
func (Gallery) CaddyModule() caddy.ModuleInfo
func (*Gallery) Provision ¶
Provision sets up the module. Creates a default scan cache if one isn't already set.
func (*Gallery) ServeHTTP ¶
ServeHTTP renders the gallery for the directory at the current request path, or falls through to the next handler (typically file_server) if the request is for a file.
Path semantics after handle_path /images/* strips the prefix:
r.URL.Path = "" → render gallery for root r.URL.Path = "subdir" → render gallery for subdir r.URL.Path = "subdir/" → render gallery for subdir r.URL.Path = "photo.jpg" → fall through to file_server r.URL.Path = "subdir/photo.jpg" → fall through to file_server r.URL.Path = "_thumbs/photo.webp" → serve as thumbnail r.URL.Path = "subdir/_thumbs/x.webp" → serve as thumbnail in subdir
On transient errors (scan failures, template parse), it falls through to the next handler rather than returning a 500.
func (*Gallery) UnmarshalCaddyfile ¶
type PageData ¶
type PageData struct {
Title string
PathPrefix string // e.g. "./" — prefix for relative links
ThumbPrefix string // e.g. "./_thumbs/" — prefix for thumb URLs
// Three sections. OtherFiles is shown in full regardless of
// pagination/sort (per the user's spec — it always appears
// at the top, horizontal). Images is the paginated/sorted
// set. The directories section is split into two
// elements: Up (the synthetic ../ entry, rendered on its
// own line, always first) and Subdirs (the actual subdirs,
// rendered in a tight row with no gap between them, per
// the user's 2026-06-17 spec).
Up *FileView // the up entry, or nil at the gallery root
Subdirs []FileView // the actual subdirs (no up entry)
OtherFiles []FileView
Images []FileView
// Pagination
Page int // 1-based
PageSize int
// TotalImages is the total media count (images + videos)
// — used for the pagination math and the visibility check
// on the images grid section.
TotalImages int
// ImageCount is the count of image files only — used for
// the "N images" label in the header meta line (so the
// label is accurate; videos are no longer miscounted as
// images). Per user request 2026-06-17: separate video
// indicator in the header.
ImageCount int
// TotalVideos is the count of video files only — shown in
// the header meta line as "N videos" (after the images
// count, only if > 0).
TotalVideos int
// TotalAllFilesSize is the pre-formatted (via humanSize) total
// size of ALL files in the directory: images + videos + other
// files. Excludes subdirectories (which don't have a Size
// field). Shown in the header meta line as a separate segment
// wrapped in `//` separators, per user request 2026-06-18:
// "the X.X KB is the total for all files in the directory"
// e.g. "34 images ·8 videos ·2 other files // (8.3 MB) //
// ·26 directories ·50 per page"
// The `//` separators visually distinguish the size from the
// other meta items (which use `·`). Operator sees at a glance
// how much disk the whole directory's media + sidecar files
// take.
TotalAllFilesSize string
TotalPages int
HasPrev bool
HasNext bool
// PageNumbers is the list of page numbers (and 0 for
// ellipsis) to show in the Google-style bottom pagination.
// Computed by pageNumbers(current, total). Empty when
// total <= 1 (no pagination needed).
PageNumbers []int
// Sort
Sort SortSpec
}
PageData is the top-level template data for a gallery page. All values are pre-formatted for direct template use — no template-level computations needed.
type ScanCache ¶
type ScanCache struct {
// contains filtered or unexported fields
}
ScanCache is a small in-memory cache of recent directory scans. Cache entries are keyed by absolute directory path; an entry is valid as long as the directory's mtime has not changed AND the entry's TTL has not expired.
The cache eliminates the per-request os.ReadDir cost in directories that don't change often (the common case for a server with photos on disk). For 100+ image directories like /images/generated, this drops per-request work from milliseconds to microseconds.
This is intentionally simple: no eviction policy beyond TTL — old entries are dropped when re-accessed after the TTL. For a server with <1000 galleries in active rotation, memory is bounded by (number of dirs) * (average file count) * (size of FileInfo).
func NewScanCache ¶
NewScanCache returns a cache with the given TTL. A TTL of 1 minute is a good default; it limits staleness if files are added/removed while also avoiding constant rescans for active directories.
type Scanner ¶
Scanner reads a directory and produces a sorted []FileInfo. Both directories and files are included; the Kind field tells them apart.
func NewScanner ¶
NewScanner returns a Scanner for the given root directory with default sort order (mtime desc — newest first).
func (*Scanner) Scan ¶
Scan walks the directory and returns a sorted slice of FileInfo. Both files and subdirectories are included (Kind = KindDir for directories). Symlinks are followed: a symlink to a directory is classified as KindDir, and the FileInfo's Size and ModTime come from the symlink's target (not the symlink itself, which would report the length of the target path string and the link's own mtime). Broken symlinks (target missing or inaccessible) are silently skipped.
Sort order:
- "mtime" (default): newest first by modification time
- "name": alphabetical by name (case-insensitive)
Returns an error only if the directory cannot be read.
type SortSpec ¶
SortSpec describes the current sort state. Field is one of "name", "type", "date", "size", or "mtime" (the default). Order is "asc" or "desc".
type ThumbConfig ¶
type ThumbConfig struct {
// Width and Height are the max-dim bounding box (in pixels)
// for the generated thumb. The source image is fit-within-
// bounds: aspect ratio is preserved and the longest edge
// becomes the configured value.
Width int
Height int
// Format is the output format: "jpeg" (or "jpg"), "png", or
// "webp" (the default, lossless). Encoded with stdlib
// image/jpeg (quality 75), stdlib image/png, or
// github.com/HugoSmits86/nativewebp respectively.
Format string
}
ThumbConfig holds the runtime configuration for thumb generation. Set at Provision time from the Gallery's Caddyfile-configured values (or defaults).