thumb

package
v0.43.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 34 Imported by: 0

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

View Source
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.

View Source
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

View Source
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

func (p *Pipeline) AttachBody(b *filebody.Resolver)

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

func (p *Pipeline) AttachStorage(id int64, drv storage.Driver)

AttachStorage registers a Driver for a storage ID — needed because the pipeline reads source bytes from the originating storage.

func (*Pipeline) CachePath

func (p *Pipeline) CachePath(nodeID int64) string

CachePath returns the disk path where a thumb is stored for node ID.

func (*Pipeline) Forget

func (p *Pipeline) Forget(ctx context.Context, nodeID int64)

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

func (p *Pipeline) GenerateThumb(ctx context.Context, node *model.Node) error

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

func (p *Pipeline) RunReaper(ctx context.Context, interval time.Duration)

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 NewSigner

func NewSigner(store SignStore, ttl time.Duration) *Signer

NewSigner returns a signer over store. ttl <= 0 means DefaultURLTTL.

func (*Signer) Query

func (s *Signer) Query(id int64) string

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

func (s *Signer) TTL() time.Duration

TTL reports the configured lifetime of a stamped URL.

func (*Signer) Verify

func (s *Signer) Verify(id int64, exp, sig string) bool

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL