media

package module
v0.6.4 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 25 Imported by: 0

README

gorest-media

A GoREST plugin that exposes files — images, videos, PDFs, CSV/XLSX spreadsheets, and any other format — as a first-class REST resource. Uploaded bytes live on local disk by default or on any CDN/object gateway, and the storage backend is swappable without touching the API.

Features

  • Any file type. Uploads are classified into a coarse kind (image, video, audio, document, spreadsheet, archive, other) from the sniffed MIME type, not the client-supplied header. New formats are onboarded through config, not code.
  • Pluggable storage. Ships with local (disk) and cdn (HTTP PUT/GET/DELETE against any S3-compatible or signed-URL gateway) drivers. Register your own with media.RegisterStorage("s3", ...).
  • Content integrity. Every object records its byte size and a SHA-256 checksum.
  • Safe by construction. MIME allow-list, size ceiling, and path-traversal-proof local keys.

Endpoints

Method Path Description
POST /media Multipart upload (file field, optional name)
GET /media List with pagination and kind/mime_type filters
GET /media/:id Metadata
GET /media/:id/download Stream the file bytes
PUT /media/:id Rename (only the display name is mutable)
DELETE /media/:id Delete the row and the stored bytes

Uploads record the authenticated user when auth middleware is mounted, and work anonymously otherwise.

Configuration

plugins:
  - name: media
    enabled: true
    config:
      storage_driver: local          # local | cdn | <custom>
      local_base_path: ./storage/media
      max_file_size: 52428800        # 50 MiB
      pagination_limit: 25
      max_pagination_limit: 100
      allowed_mime_types:            # empty = accept any type
        - image/
        - application/pdf
        - text/csv
      kind_overrides:                # classify new formats without code
        application/x-parquet: spreadsheet

For the CDN driver:

      storage_driver: cdn
      cdn_upload_url: https://storage.example.com/bucket   # PUT/DELETE target
      cdn_public_url: https://cdn.example.com/bucket       # public read URL (defaults to upload URL)
      cdn_auth_header: "Bearer <token>"                    # optional, sent on writes

Extending storage

media.RegisterStorage("s3", func(cfg *media.Config) (media.Storage, error) {
    return newS3Backend(cfg) // implement media.Storage
})

Then set storage_driver: s3. The Storage interface is four methods — Save, Open, Delete, URL — plus Driver().

Development

make test           # race + coverage
make lint
make audit

Tests run against in-memory SQLite; production targets Postgres or MySQL. The schema is provided by the plugin's migrations (migrations/) across all three dialects.

Documentation

Index

Constants

View Source
const (
	DriverLocal = "local"
	DriverCDN   = "cdn"
)
View Source
const (
	KindImage       = "image"
	KindVideo       = "video"
	KindAudio       = "audio"
	KindDocument    = "document"
	KindSpreadsheet = "spreadsheet"
	KindArchive     = "archive"
	KindOther       = "other"
)

Variables

View Source
var ErrFileTooLarge = errors.New("file exceeds maximum allowed size")

Functions

func KindForMime

func KindForMime(mime string, overrides map[string]string) string

KindForMime classifies a MIME type into a coarse media kind. Overrides win over the built-ins, so an operator can reclassify or add a format purely via config. An override key ending in "/" matches a whole family.

func NewPlugin

func NewPlugin() plugin.Plugin

func RegisterRoutes

func RegisterRoutes(router fiber.Router, db database.Database, config *Config, service *MediaService)

func RegisterStorage

func RegisterStorage(name string, factory StorageFactory)

RegisterStorage adds a storage driver under name, overwriting any existing registration. Call it from an init() to teach the plugin a new backend (e.g. an S3 client) without editing this package.

Types

type CDNStorage

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

CDNStorage speaks plain HTTP to an object gateway: PUT to write, GET to read, DELETE to remove. It carries no vendor SDK, so it works against S3-compatible endpoints, a reverse proxy, or any signed-URL gateway the operator points it at.

func NewCDNStorage

func NewCDNStorage(uploadBase, publicBase, authHeader string) *CDNStorage

func (*CDNStorage) Delete

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

func (*CDNStorage) Driver

func (s *CDNStorage) Driver() string

func (*CDNStorage) Open

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

func (*CDNStorage) Save

func (s *CDNStorage) Save(ctx context.Context, key string, r io.Reader, size int64, mimeType string) error

func (*CDNStorage) URL

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

type Config

type Config struct {
	Database database.Database

	StorageDriver string `json:"storage_driver" yaml:"storage_driver"`

	LocalBasePath string `json:"local_base_path" yaml:"local_base_path"`

	// CDNUploadURL is the base URL objects are written to (HTTP PUT/DELETE).
	// CDNPublicURL is the base URL objects are read from; when empty it falls
	// back to CDNUploadURL. Splitting them supports gateways whose write and
	// read hosts differ (e.g. a signed-upload endpoint vs. a public CDN edge).
	CDNUploadURL string `json:"cdn_upload_url" yaml:"cdn_upload_url"`
	CDNPublicURL string `json:"cdn_public_url" yaml:"cdn_public_url"`
	// CDNAuthHeader is sent verbatim as the Authorization header on write
	// requests, letting the operator supply a static token without this plugin
	// pulling in a vendor SDK.
	CDNAuthHeader string `json:"cdn_auth_header" yaml:"cdn_auth_header"`

	MaxFileSize int64 `json:"max_file_size" yaml:"max_file_size"`

	// AllowedMimeTypes gates uploads by detected MIME type. An empty list
	// accepts any type; entries ending in "/" match a whole family (e.g.
	// "image/" allows image/png and image/jpeg alike).
	AllowedMimeTypes []string `json:"allowed_mime_types" yaml:"allowed_mime_types"`

	// KindOverrides maps a MIME type (exact, or a "family/" prefix) to a media
	// kind, layered over the built-in defaults. This is the extension point for
	// classifying new formats without a code change.
	KindOverrides map[string]string `json:"kind_overrides" yaml:"kind_overrides"`

	PaginationLimit    int `json:"pagination_limit" yaml:"pagination_limit"`
	MaxPaginationLimit int `json:"max_pagination_limit" yaml:"max_pagination_limit"`
}

func DefaultConfig

func DefaultConfig() Config

func (*Config) IsAllowedMime

func (c *Config) IsAllowedMime(mime string) bool

func (*Config) Validate

func (c *Config) Validate() error

type LocalStorage

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

func NewLocalStorage

func NewLocalStorage(basePath string) (*LocalStorage, error)

func (*LocalStorage) Delete

func (s *LocalStorage) Delete(_ context.Context, key string) error

func (*LocalStorage) Driver

func (s *LocalStorage) Driver() string

func (*LocalStorage) Open

func (s *LocalStorage) Open(_ context.Context, key string) (io.ReadCloser, error)

func (*LocalStorage) Save

func (s *LocalStorage) Save(_ context.Context, key string, r io.Reader, _ int64, _ string) error

func (*LocalStorage) URL

func (s *LocalStorage) URL(string) string

type Media

type Media struct {
	ID            uuid.UUID  `json:"id" db:"id"`
	Name          string     `json:"name" db:"name"`
	StorageKey    string     `json:"storage_key" db:"storage_key"`
	StorageDriver string     `json:"storage_driver" db:"storage_driver"`
	MimeType      string     `json:"mime_type" db:"mime_type"`
	Kind          string     `json:"kind" db:"kind"`
	Extension     string     `json:"extension" db:"extension"`
	Size          int64      `json:"size" db:"size"`
	Checksum      string     `json:"checksum" db:"checksum"`
	UserID        uuid.UUID  `json:"user_id" db:"user_id"`
	CreatedAt     time.Time  `json:"created_at" db:"created_at"`
	UpdatedAt     *time.Time `json:"updated_at,omitempty" db:"updated_at"`
}

func (Media) TableName

func (Media) TableName() string

type MediaConverter

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

func NewMediaConverter

func NewMediaConverter(storage Storage) *MediaConverter

func (*MediaConverter) CreateDTOToModel

func (c *MediaConverter) CreateDTOToModel(struct{}) Media

CreateDTOToModel and UpdateDTOToModel exist only to satisfy the processor's ModelConverter interface. Writes never flow through the processor: uploads are multipart (handled by MediaService.Upload) and renames go through MediaService.Rename, so the processor is wired for list/get reads only.

func (*MediaConverter) ModelToResponseDTO

func (c *MediaConverter) ModelToResponseDTO(model Media) MediaResponseDTO

func (*MediaConverter) ModelsToResponseDTOs

func (c *MediaConverter) ModelsToResponseDTOs(models []Media) []MediaResponseDTO

func (*MediaConverter) UpdateDTOToModel

func (c *MediaConverter) UpdateDTOToModel(dto MediaUpdateDTO) Media

type MediaPlugin

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

func (*MediaPlugin) Dependencies

func (p *MediaPlugin) Dependencies() []string

func (*MediaPlugin) GetOpenAPIResources

func (p *MediaPlugin) GetOpenAPIResources() []plugin.OpenAPIResource

func (*MediaPlugin) GetService

func (p *MediaPlugin) GetService() *MediaService

func (*MediaPlugin) Handler

func (p *MediaPlugin) Handler() fiber.Handler

func (*MediaPlugin) Initialize

func (p *MediaPlugin) Initialize(config map[string]any) error

func (*MediaPlugin) MigrationDependencies

func (p *MediaPlugin) MigrationDependencies() []string

func (*MediaPlugin) MigrationSource

func (p *MediaPlugin) MigrationSource() any

func (*MediaPlugin) Name

func (p *MediaPlugin) Name() string

func (*MediaPlugin) SetupEndpoints

func (p *MediaPlugin) SetupEndpoints(router fiber.Router) error

type MediaResponseDTO

type MediaResponseDTO struct {
	ID        uuid.UUID  `json:"id"`
	Name      string     `json:"name"`
	MimeType  string     `json:"mime_type"`
	Kind      string     `json:"kind"`
	Extension string     `json:"extension"`
	Size      int64      `json:"size"`
	Checksum  string     `json:"checksum"`
	URL       string     `json:"url"`
	UserID    uuid.UUID  `json:"user_id"`
	CreatedAt time.Time  `json:"created_at"`
	UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

type MediaService

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

func NewMediaService

func NewMediaService(db database.Database, config *Config, storage Storage) *MediaService

func (*MediaService) Delete

func (s *MediaService) Delete(ctx context.Context, m *Media) error

Delete removes the database row first; the stored bytes are best-effort cleaned up afterward so a storage hiccup can't leave a dangling row that the API still advertises.

func (*MediaService) GetByID

func (s *MediaService) GetByID(ctx context.Context, id uuid.UUID) (*Media, error)

func (*MediaService) Open

func (s *MediaService) Open(ctx context.Context, m *Media) (io.ReadCloser, error)

func (*MediaService) Rename

func (s *MediaService) Rename(ctx context.Context, id uuid.UUID, name string) (*Media, error)

Rename updates only the display name (and updated_at) with a targeted UPDATE. A full-row CRUD update would rewrite the immutable storage columns from a partial DTO and could blank them, so the mutable field is written directly.

func (*MediaService) Storage

func (s *MediaService) Storage() Storage

func (*MediaService) Upload

func (s *MediaService) Upload(ctx context.Context, header *multipart.FileHeader, name string, userID uuid.UUID) (*Media, error)

Upload reads the multipart file fully into memory (bounded by MaxFileSize), sniffs its real MIME type from the bytes rather than trusting the client header, persists the bytes to the storage backend, then records the row. Buffering is deliberate: the checksum and content sniff both need to see the bytes before the write, and the size ceiling keeps the buffer bounded.

type MediaUpdateDTO

type MediaUpdateDTO struct {
	Name string `json:"name"`
}

type Storage

type Storage interface {
	Driver() string
	Save(ctx context.Context, key string, r io.Reader, size int64, mimeType string) error
	Open(ctx context.Context, key string) (io.ReadCloser, error)
	Delete(ctx context.Context, key string) error
	// URL returns a publicly reachable address for the object, or "" when the
	// backend has none and bytes must be streamed through the download route.
	URL(key string) string
}

Storage is the pluggable backend that holds the raw bytes of a media object. The database only ever stores a key; resolving that key to bytes (or a public URL) is entirely the backend's concern, which is what lets the same media resource live on local disk or a CDN without touching the HTTP layer.

func NewStorage

func NewStorage(cfg *Config) (Storage, error)

type StorageFactory

type StorageFactory func(cfg *Config) (Storage, error)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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