Asset Management#
Storyden's asset system handles user-uploaded files across posts, library nodes, links, and branding. The core domain model, storage pipeline, and safety infrastructure are split across a few focused packages.
Core Domain Model#
The Asset struct holds an AssetID (an xid.ID), a Filename, byte Size, MIME type, free-form Metadata, and an optional Parent asset reference — enabling versioned or derived assets .
Metadata is a map[string]any stored as a JSON column in the database . Typed accessor helpers GetWidth() and GetHeight() read image dimensions out of this map as float64, returning 0.0 when absent.
Filename is a value type (app/resources/asset/filename.go) that slugifies and prefixes names with an xid ID (e.g., <id>-<slug>). BuildAssetPath resolves a filename to its object-storage path under the assets/ subdirectory.
Database Schema#
The Ent schema (internal/ent/schema/asset.go) defines:
| Field | Type | Notes |
|---|---|---|
filename | string | indexed |
size | int | default 0 |
mime_type | string | default application/octet-stream |
metadata | map[string]any (JSON) | optional |
parent_asset_id | xid.ID | optional, self-referential for versions |
Assets are owned by an Account and relate to Post, Node, Link, and Event edges .
Upload Pipeline#
app/services/asset/asset_upload/uploader.go is the general-purpose upload entry point. On each call to Upload, it:
- Reads the authenticated account from context
- Detects the MIME type from the byte stream
- Persists asset metadata to the database via
asset_writer.Writer— callingAddfor new assets orAddVersionwhen aParentIDis provided - Writes the raw bytes to object storage via
object.Storer
Note that the general uploader does not perform image decoding or dimension extraction. Width/height metadata must be populated by the caller before or after the upload step if needed.
Branding Image Processing Pipeline#
Branding assets (site icons and OpenGraph banners) go through a dedicated resize pipeline before storage:
- Icon service (
app/services/branding/icon/service.go): decodes the upload, then usesimaging.Resizeto generate multiple icon sizes (512×512, 180×180, 167×167, 152×152, 120×120, 32×32) encoded as PNG . - Banner service (
app/services/branding/banner/opengraph.go): decodes the upload and resizes to 1200×630 (the standard OpenGraph dimension) .
Both services route the incoming reader through imagesafe.Decode before any resize operation.
Decompression Bomb Protection (imagesafe)#
internal/infrastructure/imagesafe/imagesafe.go is a safety wrapper around Go's standard image.Decode. Its Decode(r io.Reader) function:
- Caps encoded input at
maxEncodedBytes = 64 MiBusingio.LimitReaderbefore buffering - Reads only the image header with
image.DecodeConfigvia aTeeReader, avoiding full pixel allocation - Rejects dimension bombs: rejects non-positive dimensions and anything where
Width × Height > MaxPixels(64 million pixels) - Only then reads the full encoded body and calls
image.Decode
Errors exposed: ErrImageTooLarge and ErrEncodedTooLarge .
Why this matters: image formats (PNG, GIF) can compress large uniform images extremely well. A file of a few hundred KB with declared dimensions of 100000×100000 would allocate ~40 GB during decode. In Go, a failed allocation is fatal and takes down the whole process . This guard was added in PR #786 after the branding upload endpoints were found to decode user-supplied images without any dimension check.
Key Source Files#
| File | Purpose |
|---|---|
app/resources/asset/asset.go | Core Asset struct, Metadata type and dimension helpers |
app/resources/asset/filename.go | Filename value type, path construction |
internal/ent/schema/asset.go | Ent DB schema |
app/services/asset/asset_upload/uploader.go | General upload pipeline |
internal/infrastructure/imagesafe/imagesafe.go | Safe image decoder (decompression bomb protection) |
app/services/branding/icon/service.go | Icon resize pipeline |
app/services/branding/banner/opengraph.go | Banner resize pipeline |