DocumentsStoryden
Asset Management
Asset Management
Type
Topic
Status
Published
Created
Jul 8, 2026
Updated
Jul 8, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

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:

FieldTypeNotes
filenamestringindexed
sizeintdefault 0
mime_typestringdefault application/octet-stream
metadatamap[string]any (JSON)optional
parent_asset_idxid.IDoptional, 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:

  1. Reads the authenticated account from context
  2. Detects the MIME type from the byte stream
  3. Persists asset metadata to the database via asset_writer.Writer — calling Add for new assets or AddVersion when a ParentID is provided
  4. 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:

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:

  1. Caps encoded input at maxEncodedBytes = 64 MiB using io.LimitReader before buffering
  2. Reads only the image header with image.DecodeConfig via a TeeReader, avoiding full pixel allocation
  3. Rejects dimension bombs: rejects non-positive dimensions and anything where Width × Height > MaxPixels (64 million pixels)
  4. 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#

FilePurpose
app/resources/asset/asset.goCore Asset struct, Metadata type and dimension helpers
app/resources/asset/filename.goFilename value type, path construction
internal/ent/schema/asset.goEnt DB schema
app/services/asset/asset_upload/uploader.goGeneral upload pipeline
internal/infrastructure/imagesafe/imagesafe.goSafe image decoder (decompression bomb protection)
app/services/branding/icon/service.goIcon resize pipeline
app/services/branding/banner/opengraph.goBanner resize pipeline
Asset Management | Dosu