Media
Storage, Assets, And Media
Understand GoLazy object storage, asset export, file catalogs, and generated media variants.
The layers
The media stack is deliberately split:
lazystoragestores object bytes by key.lazyassetsregisters, fingerprints, serves, and exports application assets.lazyfilesgives stored bytes stable file IDs and fallback URLs.lazymediarecords generated variants such as thumbnails or previews.
Keep those layers separate in application services. Controllers should call product-level operations such as "store avatar" or "preview document", not assemble storage keys, write repositories, and generate variants inline.
Storage
golazy.dev/lazystorage is the byte layer. Its smallest interface opens an
object:
type Storage interface {
Open(context.Context, string, ...any) (lazystorage.File, []any, error)
}
Optional interfaces add capabilities:
type Writer interface {
Put(context.Context, string, io.Reader, ...any) (lazystorage.Info, []any, error)
}
type Deleter interface {
Delete(context.Context, string, ...any) ([]any, error)
}
type Lister interface {
List(context.Context, string, ...any) (lazystorage.Iterator, []any, error)
}
type URLer interface {
URL(context.Context, string, ...any) (lazystorage.URL, []any, error)
}
Backends consume the options they understand and return the rest. Object keys
use io/fs path rules: relative, clean, no leading slash, and no ...
The local backend is lazystorage.NewFilesystem. The embedded S3-compatible
backend is golazy.dev/lazystorage/s3:
storage := s3.New(
s3.WithEndpoint(os.Getenv("ASSETS_S3_ENDPOINT")),
s3.WithRegion(os.Getenv("ASSETS_S3_REGION")),
s3.WithBucket(os.Getenv("ASSETS_S3_BUCKET")),
s3.WithCredentials(
os.Getenv("ASSETS_S3_ACCESS_KEY_ID"),
os.Getenv("ASSETS_S3_SECRET_ACCESS_KEY"),
),
s3.WithPublicBaseURL(os.Getenv("ASSETS_PUBLIC_URL")),
)
PostgreSQL-backed storage lives in golazy.dev/pg/pgstorage; read
PostgreSQL Storage.
Assets
golazy.dev/lazyassets owns application assets, not uploaded user files. A
Registry maps logical paths such as /styles.css to registered assets and,
in production mode, to content-hashed permanent paths with ETags and integrity
metadata.
Generated asset packages can implement:
type Source interface {
Assets(*lazyassets.Registry) error
}
Applications usually let lazyapp register embedded public files and generated
asset sources. The registry then serves assets through Handler and installs
template helpers:
asset_pathasset_integritystylesheetimportmappermalink
For deploys, assets can be exported to any lazystorage.Writer:
err := registry.Upload(
ctx,
storage,
lazyassets.WithUploadPrefix("assets"),
)
There is no separate asset repository backend. If the export target should be
PostgreSQL, use pgstorage.New(db) as the storage writer; read
PostgreSQL Asset Uploads.
File catalog
golazy.dev/lazyfiles assigns stable IDs and metadata to stored bytes. The
repository contract stores file records and locations:
type Repository interface {
Put(context.Context, lazyfiles.File, lazyfiles.Location, ...any) (lazyfiles.File, []any, error)
Find(context.Context, lazyfiles.Query, ...any) (lazyfiles.File, []lazyfiles.Location, []any, error)
Delete(context.Context, string, ...any) ([]any, error)
}
Files combines a repository with named lazystorage backends:
files := &lazyfiles.Files{
Repository: filesjsonlRepo,
Storages: map[string]lazystorage.Storage{
"local": lazystorage.NewFilesystem("storage/objects"),
},
DefaultStorage: "local",
RoutePrefix: "/files",
SigningKey: []byte(os.Getenv("FILE_SIGNING_KEY")),
}
The JSONL repository lives in golazy.dev/lazyfiles/jsonl. PostgreSQL-backed
catalogs live in golazy.dev/pg/pgfiles; read
PostgreSQL File Catalogs.
File URLs and controller fallback
Ask the file service for URLs:
href, _, err := files.URL(
ctx,
file.ID,
lazystorage.ExpiresIn{Duration: 15 * time.Minute},
)
If the active storage implements lazystorage.URLer, Files.URL returns the
direct backend URL. Otherwise it returns a signed fallback route under
RoutePrefix.
Install files.Handler as middleware when fallback routes should be served by
the app:
Middlewares: []lazydispatch.Middleware{
lazydispatch.MiddlewareFunc(files.Handler),
},
Controllers should still call an app service. The service can store the upload, return a file ID, and decide whether a controller should redirect, render a URL, or enqueue follow-up media work.
Media variants
golazy.dev/lazymedia records generated representations of stored files. It
does not own byte storage. It depends on a file service, a variant repository,
and a processor:
type FileStore interface {
Open(context.Context, string, ...any) (io.ReadCloser, lazymedia.File, []any, error)
Put(context.Context, io.Reader, ...any) (lazymedia.File, []any, error)
URL(context.Context, string, ...any) (string, []any, error)
}
type Repository interface {
FindVariant(context.Context, string, string, ...any) (lazymedia.Variant, []any, error)
SaveVariant(context.Context, lazymedia.Variant, ...any) (lazymedia.Variant, []any, error)
DeleteVariant(context.Context, string, string, ...any) ([]any, error)
}
type Processor interface {
Process(context.Context, lazymedia.Source, lazymedia.Request, ...any) (lazymedia.Result, []any, error)
}
Adapt lazyfiles.Files to FileStore in your app. The adapter is the right
place to translate media options into file-catalog options:
type MediaFiles struct {
Files *lazyfiles.Files
}
func (m MediaFiles) Put(ctx context.Context, body io.Reader, options ...any) (lazymedia.File, []any, error) {
if name, remaining, ok := lazystorage.Take[lazymedia.OutputFilename](options); ok {
options = append(remaining, lazyfiles.Filename{Name: name.Name})
}
file, remaining, err := m.Files.Put(ctx, body, options...)
return mediaFile(file), remaining, err
}
The JSONL variant repository lives in golazy.dev/lazymedia/jsonl.
PostgreSQL-backed variants live in golazy.dev/pg/pgmedia; read
PostgreSQL Media Variants.
Compose the service
media := &lazymedia.Media{
Files: MediaFiles{Files: files},
Repository: mediaRepo,
Processor: imageProcessor,
}
url, _, err := media.URL(ctx, lazymedia.Request{
SourceFileID: file.ID,
VariantKey: "preview",
})
The first request generates and records the variant. Later requests reuse the
ready output unless lazymedia.Regenerate{} is passed.
Helpers and controllers
Use template helpers for application assets, Files.URL for stored-file URLs,
and app-owned service methods from controllers. Keep controller code focused on
HTTP: parse the request, call the service, and render or redirect.
This keeps backend decisions in one place and lets tests swap filesystem, S3, PostgreSQL, JSONL, or in-memory fakes without changing controller actions.