Documentation
¶
Overview ¶
Package media provides high-level image and video upload, processing, and serving on top of the storage package. It handles resizing, format conversion, thumbnail generation, and URL construction for both local filesystem and S3-compatible backends.
Index ¶
- Constants
- Variables
- func DetectType(fh *multipart.FileHeader) (mediaType string, mimeType string, err error)
- type ImageRef
- type ImageSize
- type ImageStore
- func (s *ImageStore) Delete(ctx context.Context, id string) error
- func (s *ImageStore) GetMedia(id string) ImageRef
- func (s *ImageStore) GetMediaCtx(ctx context.Context, id string) ImageRef
- func (s *ImageStore) ServeHandler() echo.HandlerFunc
- func (s *ImageStore) SignedURL(ctx context.Context, path string, expiry time.Duration, ...) (string, error)
- func (s *ImageStore) Upload(ctx context.Context, fh *multipart.FileHeader) (*ImageUploadResult, error)
- func (s *ImageStore) UploadFromReader(ctx context.Context, r io.Reader, size int64) (*ImageUploadResult, error)
- func (s *ImageStore) UploadFromReaderWithID(ctx context.Context, id string, r io.Reader, size int64, overwrite bool) (*ImageUploadResult, error)
- type ImageStoreConfig
- type ImageUploadResult
- type Option
- type VideoRef
- type VideoStore
- func (s *VideoStore) Delete(ctx context.Context, id string) error
- func (s *VideoStore) GetMedia(id string) VideoRef
- func (s *VideoStore) GetMediaCtx(ctx context.Context, id string) VideoRef
- func (s *VideoStore) ServeHandler() echo.HandlerFunc
- func (s *VideoStore) SignedURL(ctx context.Context, path string, expiry time.Duration, ...) (string, error)
- func (s *VideoStore) Upload(ctx context.Context, fh *multipart.FileHeader) (*VideoUploadResult, error)
- func (s *VideoStore) UploadFromReader(ctx context.Context, r io.Reader, size int64) (*VideoUploadResult, error)
- func (s *VideoStore) UploadFromReaderWithID(ctx context.Context, id string, r io.Reader, size int64, overwrite bool) (*VideoUploadResult, error)
- type VideoStoreConfig
- type VideoTranscodeOptions
- type VideoUploadResult
Constants ¶
const ( KB int64 = 1024 MB int64 = 1024 * 1024 GB int64 = 1024 * 1024 * 1024 )
Size constants.
const ( FormatWebP = "webp" FormatJPEG = "jpeg" FormatPNG = "png" )
Output format constants.
const ( TypeImage = "image" TypeVideo = "video" )
MediaType distinguishes images from videos.
Variables ¶
var ( ErrFileTooLarge = errors.New("media: file too large") ErrUnknownType = errors.New("media: unsupported file type") ErrVideoTooLong = errors.New("media: video exceeds maximum duration") ErrFFmpegNotFound = errors.New("media: ffmpeg not found in PATH") // ErrInvalidID is returned by *FromReaderWithID methods when the // caller-supplied id is not a canonical UUID. The check is deliberately // strict: the id is interpolated into storage paths, so allowing free-form // strings opens path-traversal and collision footguns. Callers using a // non-UUID identifier scheme should generate a UUID for storage and // keep their own id as a separate column. ErrInvalidID = errors.New("media: invalid id (must be a canonical UUID)") // ErrIDExists is returned by *FromReaderWithID methods when storage // already holds bytes under the given id and overwrite=false. Pass // overwrite=true to clobber an existing prefix (e.g. retrying after a // failed partial write). ErrIDExists = errors.New("media: id already exists") )
Sentinel errors.
var ( SizesAvatar = []ImageSize{ {Name: "thumb", Width: 64, Height: 64}, {Name: "small", Width: 150, Height: 150}, {Name: "medium", Width: 400, Height: 400}, } SizesCard = []ImageSize{ {Name: "thumb", Width: 64, Height: 64}, {Name: "small", Width: 150, Height: 150}, {Name: "medium", Width: 400, Height: 400}, {Name: "large", Width: 800, Height: 800}, {Name: "xlarge", Width: 1200, Height: 1200}, } SizesIcon = []ImageSize{ {Name: "small", Width: 100, Height: 100}, {Name: "medium", Width: 200, Height: 200}, {Name: "large", Width: 400, Height: 400}, } SizeOriginal = []ImageSize{ {Name: "original", Width: 0, Height: 0}, } )
Preset size sets.
Functions ¶
func DetectType ¶
func DetectType(fh *multipart.FileHeader) (mediaType string, mimeType string, err error)
DetectType sniffs the MIME type from the file header and returns the media type (TypeImage or TypeVideo), the MIME string, or ErrUnknownType.
Types ¶
type ImageRef ¶
type ImageRef struct {
// contains filtered or unexported fields
}
ImageRef provides URL construction for a specific uploaded image. It is returned by GetMedia / GetMediaCtx on image stores.
type ImageStore ¶
type ImageStore struct {
// contains filtered or unexported fields
}
ImageStore handles image upload, processing, serving, and deletion.
func NewLocalImageStore ¶
func NewLocalImageStore(store *storage.LocalStorage, urlPrefix string, cfg ImageStoreConfig, opts ...Option) (*ImageStore, error)
NewLocalImageStore creates an ImageStore backed by local filesystem storage. urlPrefix is the URL path prefix used to serve files (e.g. "/uploads").
func NewS3ImageStore ¶
func NewS3ImageStore(store *storage.S3Storage, cfg ImageStoreConfig, opts ...Option) (*ImageStore, error)
NewS3ImageStore creates an ImageStore backed by S3-compatible storage. If cfg.BaseURL is set, public URLs use it as the base. If cfg.SignedExpiry is set, GetMediaCtx returns pre-signed URLs.
func (*ImageStore) Delete ¶
func (s *ImageStore) Delete(ctx context.Context, id string) error
Delete removes all size variants for the given media ID.
func (*ImageStore) GetMedia ¶
func (s *ImageStore) GetMedia(id string) ImageRef
GetMedia returns an ImageRef for constructing public URLs. For local stores it uses the URL prefix; for S3 stores with BaseURL it uses that.
func (*ImageStore) GetMediaCtx ¶
func (s *ImageStore) GetMediaCtx(ctx context.Context, id string) ImageRef
GetMediaCtx returns an ImageRef that may use signed URLs for S3 stores with SignedExpiry configured. For local stores it behaves identically to GetMedia.
func (*ImageStore) ServeHandler ¶
func (s *ImageStore) ServeHandler() echo.HandlerFunc
ServeHandler returns an Echo handler that serves image files from the store. For local stores it serves from the filesystem. For S3 stores it proxies through the storage layer.
func (*ImageStore) SignedURL ¶
func (s *ImageStore) SignedURL(ctx context.Context, path string, expiry time.Duration, opts ...storage.SignOption) (string, error)
SignedURL generates a pre-signed URL for a specific storage path with a custom expiry. Only works with S3-backed stores. Options such as storage.WithAttachment are forwarded to the underlying SignURL.
func (*ImageStore) Upload ¶
func (s *ImageStore) Upload(ctx context.Context, fh *multipart.FileHeader) (*ImageUploadResult, error)
Upload processes and stores an image from a multipart file header.
func (*ImageStore) UploadFromReader ¶
func (s *ImageStore) UploadFromReader(ctx context.Context, r io.Reader, size int64) (*ImageUploadResult, error)
UploadFromReader processes and stores an image from an io.Reader. The id is generated internally as a fresh UUID. size is the upper-bound byte count from the caller's perspective; the actual buffer is checked against MaxSize after MIME detection.
func (*ImageStore) UploadFromReaderWithID ¶
func (s *ImageStore) UploadFromReaderWithID(ctx context.Context, id string, r io.Reader, size int64, overwrite bool) (*ImageUploadResult, error)
UploadFromReaderWithID processes and stores an image, using the supplied id as the storage path component instead of generating one internally.
The id MUST be in the 36-char canonical UUID form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` — the value of `id` is interpolated into storage paths, so loose validation would open the door to traversal (`"../foo"`), key collisions, and silent overwrite of unrelated assets. Other forms accepted by uuid.Parse (hyphenless 32-char, urn:uuid:..., {...} braced) are deliberately rejected: they'd let the same logical UUID land at two different storage keys. The empty string is also rejected — the whole point of *WithID is "the caller supplies the id," so falling through to UUID generation would make the method name a lie. All bad inputs return ErrInvalidID.
If storage already holds bytes under the id (e.g. a previous attempt partially wrote variants and didn't clean up), the call returns ErrIDExists unless overwrite=true. Workers retrying a failed job typically want overwrite=true so the retry replaces any residue. ErrIDExists is a best-effort precheck: concurrent *WithID calls with the same id and overwrite=false can both pass the existence check and race on Save (last-write-wins on S3, clobber on local FS). Callers that need hard exclusion should serialize at the source-of-truth layer — typically a unique constraint on the DB row that owns the id.
On ErrFileTooLarge (size pre-check), ErrInvalidID, and ErrIDExists the reader r is NOT consumed — all three errors are detected before detectMIME runs, so the rejection path doesn't drain the payload. Callers relying on the upload to drain a network or pipe reader must discard or reuse r explicitly on these errors. (ErrFileTooLarge can also fire AFTER detectMIME, when the buffered byte count exceeds MaxSize; in that case r has been drained by the MIME sniff.)
On any error after the first variant has been written, the method makes a best-effort attempt to delete the variants it has already saved before returning. Cleanup failures are logged via the configured slog logger and do not mask the original error.
type ImageStoreConfig ¶
type ImageStoreConfig struct {
Category string
Sizes []ImageSize
Quality int
Format string
MaxSize int64
SignedExpiry time.Duration
BaseURL string
}
ImageStoreConfig configures an image store.
type ImageUploadResult ¶
type ImageUploadResult struct {
ID string
MediaType string
MimeType string
// contains filtered or unexported fields
}
ImageUploadResult is returned after a successful image upload.
func (*ImageUploadResult) Path ¶
func (r *ImageUploadResult) Path(size string) string
Path returns the storage path for the given size variant.
type VideoRef ¶
type VideoRef struct {
// contains filtered or unexported fields
}
VideoRef provides URL construction for a specific uploaded video. It is returned by GetMedia / GetMediaCtx on video stores.
type VideoStore ¶
type VideoStore struct {
// contains filtered or unexported fields
}
VideoStore handles video upload, processing, serving, and deletion.
func NewLocalVideoStore ¶
func NewLocalVideoStore(store *storage.LocalStorage, urlPrefix string, cfg VideoStoreConfig, opts ...Option) (*VideoStore, error)
NewLocalVideoStore creates a VideoStore backed by local filesystem storage.
func NewS3VideoStore ¶
func NewS3VideoStore(store *storage.S3Storage, cfg VideoStoreConfig, opts ...Option) (*VideoStore, error)
NewS3VideoStore creates a VideoStore backed by S3-compatible storage.
func (*VideoStore) Delete ¶
func (s *VideoStore) Delete(ctx context.Context, id string) error
Delete removes the video and its thumbnail for the given media ID.
func (*VideoStore) GetMedia ¶
func (s *VideoStore) GetMedia(id string) VideoRef
GetMedia returns a VideoRef for constructing public URLs.
func (*VideoStore) GetMediaCtx ¶
func (s *VideoStore) GetMediaCtx(ctx context.Context, id string) VideoRef
GetMediaCtx returns a VideoRef that may use signed URLs for S3 stores.
func (*VideoStore) ServeHandler ¶
func (s *VideoStore) ServeHandler() echo.HandlerFunc
ServeHandler returns an Echo handler that serves video files from the store.
func (*VideoStore) SignedURL ¶
func (s *VideoStore) SignedURL(ctx context.Context, path string, expiry time.Duration, opts ...storage.SignOption) (string, error)
SignedURL generates a pre-signed URL for a specific storage path. Options such as storage.WithAttachment are forwarded to the underlying SignURL.
func (*VideoStore) Upload ¶
func (s *VideoStore) Upload(ctx context.Context, fh *multipart.FileHeader) (*VideoUploadResult, error)
Upload processes and stores a video from a multipart file header.
func (*VideoStore) UploadFromReader ¶
func (s *VideoStore) UploadFromReader(ctx context.Context, r io.Reader, size int64) (*VideoUploadResult, error)
UploadFromReader processes and stores a video from an io.Reader. The id is generated internally as a fresh UUID.
func (*VideoStore) UploadFromReaderWithID ¶
func (s *VideoStore) UploadFromReaderWithID(ctx context.Context, id string, r io.Reader, size int64, overwrite bool) (*VideoUploadResult, error)
UploadFromReaderWithID processes and stores a video using the supplied id as the storage path component instead of generating one internally.
The id MUST be in the 36-char canonical UUID form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. Other forms accepted by uuid.Parse (hyphenless, urn, braced) and the empty string are rejected with ErrInvalidID — same contract as the image variant; see ImageStore.UploadFromReaderWithID for the rationale.
If storage already holds bytes under the id, the call returns ErrIDExists unless overwrite=true. Workers retrying a failed transcode typically want overwrite=true so the retry replaces any residue. ErrIDExists is a best-effort precheck — see ImageStore.UploadFromReaderWithID for the concurrent-callers caveat.
On ErrFileTooLarge (size pre-check), ErrInvalidID, and ErrIDExists the reader r is NOT consumed — all three errors are detected before detectMIME / ffprobe run, so the rejection path doesn't drain the payload. Callers relying on the upload to drain a network or pipe reader must discard or reuse r explicitly on these errors. (ErrFileTooLarge can also fire after the MIME sniff against the actual buffered byte count; in that case r has been drained.)
Thumbnail generation/save failures are logged and ignored — the video bytes are still stored, and result.ThumbnailPath stays empty when this branch fails. There is no partial-write window because the video Save is the only step that can fail after the id is fixed.
type VideoStoreConfig ¶
type VideoStoreConfig struct {
Category string
// MaxSize is the maximum upload size in bytes. It's checked
// against the input payload, before any transcode runs — we
// reject oversized uploads at the door rather than after spending
// CPU. The encoded output is *not* re-checked: a pathological
// input (low-bitrate exotic codec) can in principle balloon
// past MaxSize once re-encoded at CRF 23, so callers with hard
// storage budgets should size MaxSize conservatively or pair it
// with quota tracking at the storage layer.
MaxSize int64
MaxDuration float64
GenerateThumbnail bool
ThumbnailWidth int
BaseURL string
SignedExpiry time.Duration
Transcode VideoTranscodeOptions
}
VideoStoreConfig configures a video store.
Transcode is opt-in via population: a zero-valued Transcode means uploads are stored verbatim (the original behaviour); any non-zero field flips the store into "transcode every upload to H.264/AAC MP4 before saving" mode. See VideoTranscodeOptions for the knobs and the defaults applied to fields the caller leaves unset.
type VideoTranscodeOptions ¶
type VideoTranscodeOptions struct {
// CRF is the H.264 constant-rate-factor. Effective range is 1–51;
// lower = higher quality and larger files. 18–28 is the practical
// band; the package default is 23 (the x264 baseline).
//
// CRF=0 is **not** lossless mode here even though libx264 accepts
// it as such: 0 is the Go zero value and we use it as the "caller
// did not set this" sentinel for the default-fill path, so passing
// CRF=0 ends up as CRF=23 on the encoder. Lossless H.264 for web
// video is vanishingly rare and produces huge files, and the clean
// default-fill API is worth losing that one edge case. Callers who
// genuinely need lossless transcoding should run ffmpeg themselves
// and feed the result to UploadFromReader with Transcode left zero.
CRF int
// Preset is the x264 speed/compression tradeoff. One of: ultrafast,
// superfast, veryfast, faster, fast, medium, slow, slower, veryslow,
// placebo. Default: "medium".
Preset string
// AudioBitrate is passed verbatim to ffmpeg's -b:a flag (e.g.
// "128k", "192k"). Default: "128k".
AudioBitrate string
// MaxWidth and MaxHeight clamp output dimensions while preserving
// aspect ratio. Zero on either axis means "no clamp on this axis".
// When both are zero the source dimensions are preserved.
//
// IMPORTANT: setting either of these on its own is enough to flip
// IsZero() to false and activate the full transcode pass — there
// is no "clamp size but otherwise leave bytes alone" mode. If you
// only want size clamping, you still get an H.264/AAC re-encode
// of every upload. Callers who pass through known-good MP4 should
// either leave Transcode entirely zero, or accept the re-encode.
MaxWidth, MaxHeight int
}
VideoTranscodeOptions configures the optional H.264/AAC MP4 transcode pass run by VideoStore.Upload* before bytes are persisted.
Activation is by population, not a flag: a zero-value struct means "do not transcode, save the upload bytes verbatim". As soon as any field is non-zero the transcode pass runs on every upload, and any fields left at their zero value are filled in with sensible defaults during store construction (see validate()).
The transcoder pins libx264 with `-pix_fmt yuv420p` and `-profile:v high` for broad browser/QuickTime/smart-TV compatibility (Safari and many TV decoders refuse 4:2:2 H.264, which iPhone 4K footage commonly produces); these aren't configurable on purpose. The output container is MP4 with `+faststart` so the moov atom lands at the front and the file streams progressively.
func (VideoTranscodeOptions) IsZero ¶
func (o VideoTranscodeOptions) IsZero() bool
IsZero reports whether the options struct is in its zero state, i.e. the caller has not requested transcoding. Used by VideoStore.upload to decide between "save raw bytes" and "transcode then save".
type VideoUploadResult ¶
type VideoUploadResult struct {
ID string
MediaType string
MimeType string
Duration float64
FileSize int64
ThumbnailPath string
// contains filtered or unexported fields
}
VideoUploadResult is returned after a successful video upload.
func (*VideoUploadResult) Path ¶
func (r *VideoUploadResult) Path() string
Path returns the storage path for the video file.