Documentation
¶
Overview ¶
Package thumb generates and serves thumbnail images for nodes.
The pipeline is dispatcher-based: GenerateThumb inspects the source node's mime type and routes to the appropriate generator (image / video / pdf / office). Each generator writes an output JPEG/PNG to the configured cache storage and updates the thumbnails table.
Generators that require external binaries (ffmpeg, gs, libreoffice) detect availability up-front via the capability package and gracefully skip when not present.
Index ¶
- Constants
- Variables
- type Capabilities
- type Pipeline
- func (p *Pipeline) AttachBody(b *filebody.Resolver)
- func (p *Pipeline) AttachStorage(id int64, drv storage.Driver)
- func (p *Pipeline) CachePath(nodeID int64) string
- func (p *Pipeline) Forget(ctx context.Context, nodeID int64)
- func (p *Pipeline) GenerateThumb(ctx context.Context, node *model.Node) error
- func (p *Pipeline) ReapOrphans(ctx context.Context) (ReapResult, error)
- func (p *Pipeline) RefitOversized(ctx context.Context) (RefitResult, error)
- func (p *Pipeline) RunReaper(ctx context.Context, interval time.Duration)
- type ReapResult
- type RefitResult
- type SignStore
- type Signer
Constants ¶
const DefaultURLTTL = 24 * time.Hour
DefaultURLTTL is how long a stamped thumbnail URL stays valid.
⚠ It matches the `Cache-Control: private, max-age=86400` the endpoint sends: a signature that expired before the browser cache did would produce a thumbnail that renders from cache on one machine and 403s on another, which is the kind of difference nobody can reproduce.
const SigningKeySetting = "thumb_signing_key"
SigningKeySetting is the settings row holding the HMAC key. Generated on first use and then stable — rotating it only invalidates outstanding URLs, it never locks anybody out (authenticated callers do not need one).
Variables ¶
var ErrSkipped = errors.New("thumb: skipped")
ErrSkipped is returned when no generator applies to the node — caller should mark the node's thumb state as "skipped" rather than "failed".
Functions ¶
This section is empty.
Types ¶
type Capabilities ¶
type Capabilities struct {
Image bool // always true (Go stdlib + bundled imaging)
Video bool // ffmpeg present in PATH
Audio bool // ffmpeg present (same binary handles audio waveform)
PDF bool // ghostscript or pdftoppm present
Office bool // libreoffice/soffice present
SVG bool // rsvg-convert present (vector→raster)
}
Capabilities indicates which thumbnail backends are available at runtime.
type Pipeline ¶
type Pipeline struct {
// contains filtered or unexported fields
}
Pipeline coordinates thumbnail generation.
func New ¶
func New(store db.Store, cacheDir string, caps Capabilities) *Pipeline
New constructs a Pipeline.
func (*Pipeline) AttachBody ¶
AttachBody wires the byte-source resolver, so a file that is still being transferred gets its thumbnail from the staged bytes instead of failing against a driver that does not have the object yet.
func (*Pipeline) AttachStorage ¶
AttachStorage registers a Driver for a storage ID — needed because the pipeline reads source bytes from the originating storage.
func (*Pipeline) Forget ¶
Forget removes one node's cached thumbnail — the JPEG and the row.
Call it where a node is destroyed permanently (trash purge, retention expiry, a hard delete on a driver that cannot preserve bytes). It is NOT for a soft delete: a trashed file is restorable and keeps its thumbnail.
Best-effort by design: a thumbnail is a cache, and failing to remove one must never fail the deletion it is attached to. Every failure is logged.
func (*Pipeline) GenerateThumb ¶
GenerateThumb dispatches based on node MIME and updates the thumbnails row. Idempotent — safe to call repeatedly.
Falls back to the file extension when `node.Mime` is empty — this is the common case for files discovered by sync's driver.List, where most drivers don't populate the mime field. Without the fallback every synced file got `state=skipped` and the pipeline never produced a thumbnail for the demo fixtures.
func (*Pipeline) ReapOrphans ¶
func (p *Pipeline) ReapOrphans(ctx context.Context) (ReapResult, error)
ReapOrphans deletes cached thumbnails whose node no longer exists.
See the file comment for the four conditions a file has to meet. Returns the tally so the caller can log it; an error means the pass was ABANDONED and nothing was deleted.
func (*Pipeline) RefitOversized ¶
func (p *Pipeline) RefitOversized(ctx context.Context) (RefitResult, error)
RefitOversized scales down every cached thumbnail wider than thumbMaxWidth. See the comment above for which files it touches. An error means the pass stopped early (the directory could not be read, or ctx ended); files already rewritten stay rewritten.
func (*Pipeline) RunReaper ¶
RunReaper reconciles once at boot and then on every tick until ctx is done. The boot pass is followed by one RefitOversized pass (refit.go): the files it rewrites can only come from a version before 0.41.0, so once per start is enough and the ticks stay the cheap orphan sweep they always were.
interval <= 0 disables it entirely (FILEX_THUMBS_SWEEP_INTERVAL=0) — a kill switch for an operator who would rather run `filex thumb` maintenance by hand than have the server touch the cache directory at all.
⚠ One log line per pass, including the quiet ones. The staged-upload sweeper learned this the expensive way: a sweeper that only speaks when it deletes something is indistinguishable from a sweeper that has stopped running.
type ReapResult ¶
type ReapResult struct {
Scanned int // candidate files inspected
Removed int // files deleted
Freed int64 // bytes reclaimed
Kept int // files whose node still exists (live, or trashed and restorable)
Skipped int // files too fresh to judge, or unparseable names
}
ReapResult is what one reconciliation pass did.
type RefitResult ¶
type RefitResult struct {
Scanned int // cache files whose dimensions were read and judged
Refitted int // files rewritten at thumbnail size
Freed int64 // bytes the rewrites saved
Failed int // oversized files that could not be rewritten (left as they were)
Skipped int // not ours, too fresh, not a readable JPEG, or too large to decode safely
}
RefitResult is what one re-fit pass did.
type SignStore ¶
type SignStore interface {
GetSetting(ctx context.Context, key string) (string, error)
UpsertSetting(ctx context.Context, key, value string) error
}
SignStore is the slice of db.Store the signer needs. Narrow on purpose so the signer is testable without a database.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer mints and verifies thumbnail URL signatures.
The zero value is not usable; a nil *Signer is, and it is the fail-CLOSED state: Query returns no signature and Verify refuses everything, so an install that never wired one simply requires an authenticated caller.
func (*Signer) Query ¶
Query returns the `exp=…&sig=…` fragment for node id, or "" when no signature can be produced (nil signer, or the key could not be loaded — callers must then fall back to authenticated fetching rather than emit an unsigned URL that would only 401).
⚠ The expiry is QUANTIZED to a bucket rather than being `now + ttl`. An expiry that moved on every listing would change the URL string on every listing, and the URL string is the cache key both for the browser's HTTP cache and for useThumbs' object-URL cache — every folder refresh would re-download every thumbnail. Quantizing makes the URL stable within a window at the cost of up to one bucket of extra lifetime.
func (*Signer) Verify ¶
Verify reports whether (exp, sig) is a live signature for node id.
A mismatch triggers ONE reload of the key from the settings row before the refusal: another process (a second replica, or an operator rotating the row) may have changed it since this process cached it, and a stale cache would otherwise reject signatures the deployment is currently minting.