media

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 42 Imported by: 0

Documentation

Overview

Package media stores shared assets (images and documents) with translated titles and alt texts. Content references assets by ID; an asset that is still referenced (see wt_media_usage) cannot be deleted. Images can be shown in other sizes and formats through variants (see Variant).

Index

Constants

View Source
const (
	// OriginalFormat keeps the format of the upload: a PNG stays a lossless
	// PNG and a JPEG a JPEG, for images whose sharp edges lossy WebP would
	// blur, such as a logo.
	OriginalFormat = "original"
	// MailFormat is a format every e-mail program shows, which WebP is not:
	// JPEG, or PNG for a PNG with transparency, which JPEG would lose.
	MailFormat = "mail"
)

Besides the variant formats (webp, jpg and png), ImageAs takes formats that depend on the image.

View Source
const DefaultVariantFormat = "webp"

DefaultVariantFormat is the format of variants that do not name one.

View Source
const ImageWidth = 1600

ImageWidth is the largest width pages show an image in by default, as module views and content do.

View Source
const LargeImageWidth = 2400

LargeImageWidth is the width of an image seen on its own, such as in a gallery's lightbox.

View Source
const MailImageWidth = 1200

MailImageWidth is the width of images in e-mail: twice the 600 pixels most mail layouts are wide, so they stay sharp on dense screens.

View Source
const MaxVariantSize = 4000

MaxVariantSize is the largest width or height of a variant in pixels.

Variables

View Source
var (
	// ErrNotFound is an asset that is not in the library, or not in the
	// trash where that is asked.
	ErrNotFound = errors.New("media_not_found")
	// ErrInvalid is an upload without a usable file name, an empty file, an
	// image that cannot be read or has too many pixels, or a storage
	// address that is no URL.
	ErrInvalid = errors.New("invalid_media")
	// ErrTooLarge is an upload over the size limit (see MaxUploadSize).
	ErrTooLarge = errors.New("media_too_large")
	// ErrUnsupportedType is an upload whose content is no type the library
	// accepts, whatever its file name says.
	ErrUnsupportedType = errors.New("unsupported_media_type")
	// ErrInUse refuses to delete an asset that content still uses, so pages
	// never show a missing image.
	ErrInUse = errors.New("media_in_use")
	// ErrUnsafeKey is a storage key that could reach outside the media
	// directory, such as one with a slash or "..".
	ErrUnsafeKey = errors.New("unsafe_media_key")
)
View Source
var Centre = FocalPoint{X: 0.5, Y: 0.5}

Centre is the focal point of images that have none of their own.

View Source
var ErrVariant = errors.New("invalid_image_variant")

ErrVariant is returned for a variant specification that cannot be made.

View Source
var ImageWidths = []int{480, 800, 1200, 1600}

ImageWidths are the widths an image's srcset offers below its largest width (see Service.Image).

Functions

func Resize

func Resize(original []byte, variant Variant) ([]byte, error)

Resize makes a variant of an encoded JPEG, PNG or GIF image: it is turned upright following its EXIF orientation, cropped around the variant's Focus, scaled with the Catmull-Rom filter and encoded in the variant's format. An animated GIF gives its first frame.

func Schema

func Schema() fs.FS

Schema holds this package's schema files per dialect (see db.ApplySchema). It also creates wt_media_usage, which content stores write to.

func Track

func Track(ctx context.Context, database *db.DB, owner, ownerID string, assets []string) error

Track records that owner/ownerID references exactly the given assets, replacing earlier records. Referenced assets cannot be deleted, and assets in the trash cannot be referenced (ErrNotFound). Owner is usually a table name, ownerID the referencing row. Empty IDs are ignored.

Content with revisions (pages and the modules with publication, see content.Revisions) tracks every revision, not only the published one, and keeps old revisions so editors can restore them. A file that any revision uses therefore stays in use, and cannot be deleted, until the page or item itself is deleted for good (purged from the trash): restoring an old revision or a deleted item never finds its images gone.

func Untrack

func Untrack(ctx context.Context, database *db.DB, owner, ownerID string) error

Untrack removes all usage records of an owner row, for example on delete.

Types

type Asset

type Asset struct {
	ID         string `db:"id"`
	StorageKey string `db:"storage_key"`
	Filename   string `db:"filename"`
	MIMEType   string `db:"mime_type"`
	SizeBytes  int64  `db:"size_bytes"`
	// Width and Height are the size of an image as shown: upright, following
	// its EXIF orientation.
	Width  int `db:"width"`
	Height int `db:"height"`
	// FocalX and FocalY are the image's focal point (see FocalPoint and
	// SetFocalPoint).
	FocalX    float64   `db:"focal_x"`
	FocalY    float64   `db:"focal_y"`
	Checksum  string    `db:"checksum"`
	CreatedAt time.Time `db:"created_at"`
	CreatedBy db.Ref    `db:"created_by"`
	UpdatedAt time.Time `db:"updated_at"`
	UpdatedBy db.Ref    `db:"updated_by"`
	// DeletedAt is when the asset went to the trash, by DeletedBy; nil for
	// assets in the library. Only Trashed returns deleted assets.
	DeletedAt    *time.Time    `db:"deleted_at"`
	DeletedBy    db.Ref        `db:"deleted_by"`
	Translations []Translation `db:"-"`
}

Asset is one file in the media library, such as a photo or a PDF, with its texts per language. Content refers to it by ID.

func (Asset) FocalPoint

func (a Asset) FocalPoint() FocalPoint

FocalPoint is the point of the image that crops keep in view.

func (Asset) IsImage

func (a Asset) IsImage() bool

IsImage reports whether the asset is an image, which can be shown in other sizes (see Variant) and needs an alternative text.

func (Asset) Text

func (a Asset) Text(locale string) Translation

Text returns the translation for locale; empty when none was written.

type Filesystem

type Filesystem struct {
	// contains filtered or unexported fields
}

Filesystem uses os.Root so even a symlink cannot escape the asset directory. The application owns Close. Files are served by the library's routes (Service.RegisterRoutes), or at a base URL of their own.

func NewFilesystem

func NewFilesystem(directory, baseURL string) (*Filesystem, error)

NewFilesystem keeps files in directory, made when missing. An empty baseURL leaves serving them to the library's routes (see Service.RegisterRoutes), as webthings.App does; otherwise they are served at baseURL, a path on the site or an absolute http(s) address, for example by a file server or a CDN. It returns ErrInvalid for any other baseURL.

func (*Filesystem) Close

func (s *Filesystem) Close() error

Close releases the directory.

func (*Filesystem) Delete

func (s *Filesystem) Delete(ctx context.Context, key string) error

Delete implements Storage.

func (*Filesystem) Open

func (s *Filesystem) Open(ctx context.Context, key string) (io.ReadCloser, error)

Open implements Storage.

func (*Filesystem) Ping

func (s *Filesystem) Ping(context.Context) error

Ping checks that the asset directory can still be read.

func (*Filesystem) Put

func (s *Filesystem) Put(ctx context.Context, key string, reader io.Reader, size int64) error

Put implements Storage. A write that ends short of size leaves no file behind.

func (*Filesystem) URL

func (s *Filesystem) URL(key string) string

URL implements Storage: the key below the base URL, or "" without one, for the library's routes to serve.

type FocalPoint

type FocalPoint struct {
	X float64
	Y float64
}

FocalPoint is the part of an image that matters most, such as a face: every crop keeps it as near its centre as the image allows, so a point near a corner stays near that corner in every crop. X and Y are fractions of the upright image's width and height from the top left; the centre (0.5, 0.5) is the default.

func (FocalPoint) ObjectPosition

func (p FocalPoint) ObjectPosition(width, height int, box image.Point) string

ObjectPosition is a CSS object-position value for a width × height image that the browser crops with object-fit: cover. With the proportions of the box (box.X wide, box.Y high) it shows exactly what a crop variant of that size shows. Without them (a zero box) it puts the focal point as far into the box as it is into the image: always visible, and in the corner when it is in a corner, but not centred.

func (FocalPoint) PositionClass

func (p FocalPoint) PositionClass(width, height int, box image.Point) string

PositionClass is the class of focal-points.css, the stylesheet core serves to themes, that sets about ObjectPosition: "fp-<x>-<y>" with both percentages rounded to a multiple of 5, such as "fp-35-60". Unlike a style attribute, a class works under a Content-Security-Policy without 'unsafe-inline'.

func (FocalPoint) Valid

func (p FocalPoint) Valid() bool

Valid reports whether both fractions lie between 0 and 1.

func (FocalPoint) Window

func (p FocalPoint) Window(bounds image.Rectangle, size image.Point) image.Rectangle

Window places a rectangle of size within bounds so that the focal point is at its centre, moved as little as needed to stay inside bounds. A window as large as bounds on a side covers that side.

type Image

type Image struct {
	// URL is a copy at most the asked width wide, in DefaultVariantFormat
	// unless ImageAs asked for another, keeping the image's proportions and
	// never larger than the original. Images a variant would change, such
	// as a GIF that may be animated, and files outside the library keep
	// their own address, as does a PNG asked for as PNG that needs no
	// resizing: a copy would only be larger.
	URL string
	// Srcset lists copies of the image up to the width of URL, for a srcset
	// attribute; empty when URL is the original.
	Srcset string
	// OriginalURL is the uploaded file, for downloads.
	OriginalURL string
	// Width and Height are the size of URL in pixels, for an img's width and
	// height attributes; 0 when unknown.
	Width  int
	Height int
}

Image is an image as a page shows it.

type Pinger

type Pinger interface {
	Ping(ctx context.Context) error
}

Pinger is a Storage that can tell whether it can be reached, for example an object store over the network. The app's readiness check (/readyz) asks storage that implements it.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the media library: it stores uploads in Storage and their details in the database, serves them with variants, and keeps assets that content uses from being deleted. It is safe for concurrent use.

func New

func New(database *db.DB, storage Storage) *Service

New returns the media library on database, whose schema (Schema) must be applied, with the files in storage. It signs variant URLs with a random key until SetSigningKey sets a lasting one.

func (*Service) AltText

func (s *Service) AltText(ctx context.Context, ref, locale string) string

AltText is the alt text editors wrote for an image in locale, or "" when there is none. ref is an asset ID or the URL of an asset, as for VariantURL. Alt texts are kept in memory with the asset's other details: a change in this process shows at once, one by another process within a minute.

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, id string) error

Delete moves an asset that no content references to the trash: it leaves the library and the website, and Undelete brings it back. Content in the trash still references its files, so they stay (ErrInUse).

func (*Service) FocalPoint

func (s *Service) FocalPoint(ctx context.Context, ref string) FocalPoint

FocalPoint returns the focal point of an image; the centre for unknown assets. ref is an asset ID or the URL of an asset, as for VariantURL.

func (*Service) Get

func (s *Service) Get(ctx context.Context, id string) (Asset, error)

Get returns an asset in the library; assets in the trash are not found.

func (*Service) Image

func (s *Service) Image(ctx context.Context, ref string, width int) Image

Image returns how a page shows an image at most width pixels wide (for example ImageWidth), in DefaultVariantFormat. ref is an asset ID or the URL of an asset, as for VariantURL; anything else, such as a theme's own SVG logo, is shown as it is. Unknown assets give an empty Image. It only builds addresses: variants are made when they are first requested, or before by Prepare.

func (*Service) ImageAs

func (s *Service) ImageAs(ctx context.Context, ref string, width int, format string) Image

ImageAs is Image in another format: a variant format (webp, jpg or png), OriginalFormat or MailFormat. The srcset has the same format.

func (*Service) List

func (s *Service) List(ctx context.Context) ([]Asset, error)

List returns all assets, newest first, without translations (see ListWithTranslations).

func (*Service) ListWithTranslations

func (s *Service) ListWithTranslations(ctx context.Context) ([]Asset, error)

ListWithTranslations returns all assets, newest first, with their titles and alt texts in every language. It reads all translations in one query, so a library search can match them without a query per asset.

func (*Service) MaxUploadSize

func (s *Service) MaxUploadSize() int64

MaxUploadSize is the largest accepted file in bytes.

func (*Service) ObjectPosition

func (s *Service) ObjectPosition(ctx context.Context, ref, box string) string

ObjectPosition is a CSS object-position value that makes the browser's object-fit: cover crop of an image follow its focal point (see FocalPoint.ObjectPosition). box gives the proportions of the crop, as "4:3" or "800x600", to show exactly what the crop variant of that size shows; empty, the value suits boxes of any shape. Unknown assets give the centre. ref is an asset ID or the URL of an asset, as for VariantURL.

func (*Service) Open

func (s *Service) Open(ctx context.Context, key string) (Asset, io.ReadCloser, error)

Open returns an asset and its bytes by storage key. The caller closes the reader.

func (*Service) Originals

func (s *Service) Originals(ctx context.Context) ([]string, error)

Originals returns the storage keys of the uploaded files, as Storage keeps them: those of the assets in the library and in the trash, oldest first. Variants are left out, as the library makes them again on their first request; so are the files of assets deleted for good. A backup of the files needs these alone, read from Storage.

func (*Service) PositionClass

func (s *Service) PositionClass(ctx context.Context, ref, box string) string

PositionClass is the class of focal-points.css for ObjectPosition (see FocalPoint.PositionClass), such as "fp-35-60"; unknown assets give the centre, "fp-50-50". ref and box are as for ObjectPosition.

func (*Service) Prepare

func (s *Service) Prepare(ctx context.Context)

Prepare makes the variants pages show of each uploaded image, one image at a time, until ctx ends: the ImageWidth copy first, then the smaller ones of its srcset. It shares the limit on variants made at once with requests, and a request for a variant it is making waits for it rather than making it again. When ctx ends it finishes the variant it is making and drops the images still waiting. App runs it from Start until Close; without it, variants are made on their first request.

func (*Service) Purge

func (s *Service) Purge(ctx context.Context, id string) error

Purge removes an asset in the trash for good, with its file and variants. An asset that content in the trash still uses stays (ErrInUse).

func (*Service) RegisterRoutes

func (s *Service) RegisterRoutes(routes *router.Router)

RegisterRoutes registers the routes that serve the library on routes, to GET and HEAD requests: the bytes of an asset by storage key, named asset, and image variants (see Variant), named variant. The library builds the addresses of variants from them, and those of assets its Storage leaves to the site (Storage.URL returns ""), so routes must be mounted where they are served, behind X-Content-Type-Options: nosniff (go-router's middleware.SecurityHeaders), so browsers never take an upload for another type than it has. webthings.App mounts them at /media. Call it once, before routes serves.

func (*Service) Resizable

func (s *Service) Resizable(ctx context.Context, ref string) bool

Resizable reports whether variants of an asset keep what it shows: JPEG and PNG images. A GIF may be animated, which a variant would lose. ref is an asset ID or the URL of an asset, as for VariantURL.

func (*Service) SaveTranslation

func (s *Service) SaveTranslation(ctx context.Context, id string, translation Translation) error

SaveTranslation sets the title and alt text for one locale.

func (*Service) SetFocalPoint

func (s *Service) SetFocalPoint(ctx context.Context, id string, point FocalPoint) error

SetFocalPoint sets the focal point of an image (ErrInvalid for other files or a point outside the image). Crop variants around the previous point are deleted: their URLs name the focal point, so pages get new URLs at once and long-cached copies of the old ones are not used. Variants that are not cropped do not change.

func (*Service) SetSigningKey

func (s *Service) SetSigningKey(key []byte)

SetSigningKey sets the key that signs variant URLs. Every process of a site must use the same key; App uses a secret kept in the site settings. Without one the Service uses a random key, so variant URLs change on restart.

func (*Service) Srcset

func (s *Service) Srcset(ctx context.Context, ref, format string, widths ...int) (string, error)

Srcset lists variants of an image in the given widths for a srcset attribute, for example "/media/v/…_400x0.webp 400w, /media/v/…_800x0.webp 800w". Widths beyond the image's own width are left out; the image's own width is added instead, so a small image is never offered as a large one.

func (*Service) Storage

func (s *Service) Storage() Storage

Storage returns where the library keeps its files, such as a Filesystem or an object store of Config.Storage, for reading the files Originals names.

func (*Service) Trashed

func (s *Service) Trashed(ctx context.Context, limit, offset int) ([]Asset, int, error)

Trashed returns assets in the trash with their translations, most recently deleted first: at most limit of them from offset (all for a limit of 0), and how many the trash holds in all.

func (*Service) URL

func (s *Service) URL(ctx context.Context, id string) string

URL is the public address of an asset, or "" when it does not exist: the one its Storage gives, or else that of the library's route named asset (see RegisterRoutes).

func (*Service) Undelete

func (s *Service) Undelete(ctx context.Context, id string) error

Undelete brings an asset back from the trash.

func (*Service) Upload

func (s *Service) Upload(ctx context.Context, filename string, reader io.Reader) (Asset, error)

Upload stores a file. The type is detected from the bytes, not the filename. The file is kept as it was uploaded; the variants pages show of an image are made in the background when Prepare runs.

func (*Service) VariantURL

func (s *Service) VariantURL(ctx context.Context, ref string, variant Variant) (string, error)

VariantURL is the address of a variant of an image. ref is an asset ID, the URL of an asset (for example /media/<id>.jpg) or the URL of one of its variants, as module views give them (see Image). Crops keep the asset's focal point in view unless the variant names its own Focus. It returns ErrNotFound for unknown assets and ErrVariant for files that are not images. The address is that of the library's route named variant (see RegisterRoutes), whatever the Storage, because variants are made on their first request.

func (*Service) Version

func (s *Service) Version() uint64

Version changes whenever an asset changes in this process: its focal point, its titles and alt texts, or whether it is in the library at all. Caches of what is made from assets, such as rendered content, compare it to know they are out of date. Changes by other processes do not change it; the asset details themselves are read again after a minute.

func (*Service) WaitPrepared

func (s *Service) WaitPrepared(ctx context.Context) error

WaitPrepared waits until Prepare has made the variants of the images uploaded so far, or ctx ends, for tests and tools that need them stored.

type Storage

type Storage interface {
	// Put stores size bytes from reader under key, a name the Service
	// chose. It fails rather than overwrite a file.
	Put(ctx context.Context, key string, reader io.Reader, size int64) error
	// Open returns the file stored under key.
	Open(ctx context.Context, key string) (io.ReadCloser, error)
	// Delete removes the file under key; a file that is gone already is no
	// error.
	Delete(ctx context.Context, key string) error
	// URL is the address the file is served at, or "" when the site serves
	// it with the library's routes (see Service.RegisterRoutes), as it does
	// with Filesystem.
	URL(key string) string
}

Storage keeps the bytes of the files; the database keeps everything else. Filesystem is the default; a site can bring another, such as an object store, with Config.Storage.

type Translation

type Translation struct {
	Locale  string `db:"locale"`
	Title   string `db:"title"`
	AltText string `db:"alt_text"`
}

Translation is an asset's text in one language: the title shown with it, and the alternative text read to visitors who cannot see an image.

type Variant

type Variant struct {
	// Width and Height bound the size in pixels; 0 leaves that side free.
	Width  int
	Height int
	// Crop cuts the image to exactly Width × Height instead of fitting it
	// inside. It needs both sides.
	Crop bool
	// Focus is the point a crop keeps as near its centre as it can; nil is
	// the centre. VariantURL sets the asset's focal point when it is nil.
	Focus *FocalPoint
	// Format is webp (the default), jpg or png.
	Format string
}

Variant describes a resized copy of an image.

func ParseVariant

func ParseVariant(spec string) (Variant, error)

ParseVariant reads a specification as templates write it: a size, then optionally "crop" and a format, separated by spaces:

"800x600"        fit inside 800 × 600, as WebP
"800"            800 wide
"x600"           600 high
"800x600 crop"   exactly 800 × 600, cut around the focal point
"800 jpg"        800 wide, as JPEG

func (Variant) Fit

func (v Variant) Fit(width, height int) (size, part image.Point)

Fit returns the size of the variant of a width × height image, and the size of the part of the image it shows, which Resize places around the focal point (see FocalPoint.Window). Images are never made larger: a box larger than the image gives the image's own size, and a crop larger than the image cuts the largest part with the box's proportions.

Directories

Path Synopsis
Package mediaadmin is the Media section of the admin: the media library, where editors upload files, write their titles and alternative texts per language and choose the focal point of images.
Package mediaadmin is the Media section of the admin: the media library, where editors upload files, write their titles and alternative texts per language and choose the focal point of images.

Jump to

Keyboard shortcuts

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