Help us shape the future of Go! Take the 2026 Go Developer Survey

medialibrary

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 28 Imported by: 0

README

gorm-medialibrary

Spatie Laravel Media Library–compatible media attachments for GORM.

Go Tests codecov Go Reference License

Features

  • Spatie-compatible media table (collections, conversions, responsive images, UUID, order)
  • Attach from path or io.Reader
  • Named collections (SingleFile, MIME allow-list, disk selection)
  • Image conversions (fit / crop) via disintegration/imaging
  • Responsive images (width variants + tiny SVG placeholder)
  • Built-in LocalDisk and S3Disk (AWS SDK v2 / S3-compatible)
  • Safe uploads: disk I/O outside DB transactions; compensating cleanup; singleFile replaces only after success
  • Path layout: {id}/{file}, {id}/conversions/, {id}/responsive-images/
  • Go 1.24+

Install

go get github.com/noormaulida/gorm-medialibrary@latest

Quick start

disk := medialibrary.NewLocalDisk("public", "./storage/app/public", "/storage")

lib := medialibrary.New(db,
    medialibrary.WithDisk(disk),
    medialibrary.WithDefaultDisk("public"),
)
_ = medialibrary.Migrate(db) // skip if Laravel/Spatie already migrated

Model

type SeoMeta struct {
    ID    uint64
    Title string
}

func (s SeoMeta) MediaModelID() uint64   { return s.ID }
func (s SeoMeta) MediaModelType() string { return "seo_metas" } // morph alias shared with Laravel

Register

lib.Register("seo_metas", func(r *medialibrary.Registrar) {
    r.Collection("seo_image").
        SingleFile().
        AcceptsMimeTypes("image/jpeg", "image/png").
        Disk("public").
        WithResponsiveImages()

    r.Conversion("seo_image_large").
        ForCollection("seo_image").
        Crop(1200, 630).
        Quality(85).
        WithResponsiveImages()

    r.Conversion("seo_image_small").
        ForCollection("seo_image").
        Crop(240, 126)
})

Add & read

src, err := medialibrary.FromPath("./uploads/hero.jpg")
if err != nil {
    return err
}

media, err := lib.AddMedia(ctx, seoMeta, "seo_image", src)
if err != nil {
    return err
}

// Optional per-upload disk override:
// lib.AddMedia(ctx, seoMeta, "seo_image", src, medialibrary.OnDisk("s3"))

url, _ := lib.GetURL(*media)
large, _ := lib.GetURL(*media, "seo_image_large")
srcset, _ := lib.GetSrcset(*media)

Also: GetMedia, GetFirstMedia, GetFirstMediaURL, GetResponsiveImageURLs, ClearMediaCollection, Delete.

HTTP upload

file, header, _ := c.Request.FormFile("image")
defer file.Close()
src, err := medialibrary.FromReader(file, header.Filename, header.Header.Get("Content-Type"))

S3 disk

import (
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
    return err
}
client := s3.NewFromConfig(cfg)

disk, err := medialibrary.NewS3Disk(medialibrary.S3DiskConfig{
    Name:    "s3",
    Bucket:  "my-bucket",
    Prefix:  "media",
    BaseURL: "https://cdn.example.com",
    Client:  client,
})
if err != nil {
    return err
}
lib := medialibrary.New(db, medialibrary.WithDisk(disk), medialibrary.WithDefaultDisk("s3"))

Works with MinIO, R2, and other S3-compatible APIs via the AWS SDK endpoint config. For GCS or custom storage, implement medialibrary.Disk.

Disk selection

Priority (highest first): OnDisk / OnConversionsDisk → collection Disk / ConversionsDisk → library default.

r.Collection("images").Disk("s3").ConversionsDisk("s3")

lib.AddMedia(ctx, model, "images", src, medialibrary.OnDisk("s3"))

Spatie notes

Concern Guidance
Table / columns Spatie v9+ style
model_type Prefer morph alias shared with Laravel
Paths {id}/{file}, conversions/, responsive-images/
Responsive JSON Keys media_library_original / conversion name

License

MIT

Documentation

Overview

Package medialibrary associates files with GORM models using a schema compatible with Spatie Laravel Media Library.

Basic usage:

_ = medialibrary.Migrate(db)
lib := medialibrary.New(db, medialibrary.WithDisk(medialibrary.NewLocalDisk("public", "./storage", "/storage")))
lib.Register("seo_metas", func(r *medialibrary.Registrar) {
    r.Collection("seo_image").SingleFile().WithResponsiveImages()
    r.Conversion("seo_image_large").ForCollection("seo_image").Crop(1200, 630).WithResponsiveImages()
})
media, err := lib.AddMedia(ctx, model, "seo_image", medialibrary.FromPath("photo.jpg"))

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidArgument is returned for nil inputs or empty required fields.
	ErrInvalidArgument = errors.New("medialibrary: invalid argument")

	// ErrNotFound is returned when media cannot be located.
	ErrNotFound = errors.New("medialibrary: not found")

	// ErrDiskNotFound is returned when a configured disk name is missing.
	ErrDiskNotFound = errors.New("medialibrary: disk not found")

	// ErrMimeNotAllowed is returned when a file MIME type is rejected by the collection.
	ErrMimeNotAllowed = errors.New("medialibrary: mime type not allowed")

	// ErrNotImage is returned when an image conversion is requested for a non-image.
	ErrNotImage = errors.New("medialibrary: not an image")

	// ErrReplaceFailed is returned when singleFile cannot remove previous media
	// after a successful upload (the new media is rolled back when possible).
	ErrReplaceFailed = errors.New("medialibrary: failed to replace previous media")
)

Functions

func ConversionPath

func ConversionPath(m Media, conversion string) string

ConversionPath is the relative disk path for a named conversion.

func Migrate

func Migrate(db *gorm.DB) error

Migrate creates the Spatie-compatible media table when missing. Skip this when the database already uses Laravel Media Library migrations.

func OriginalPath

func OriginalPath(m Media) string

OriginalPath is the relative disk path for the original file (Spatie layout).

func ParseResponsiveWidth

func ParseResponsiveWidth(fileName string) (int, bool)

ParseResponsiveWidth extracts the width descriptor from a Spatie responsive file name.

func PrimaryKeyID

func PrimaryKeyID(model any) (uint64, error)

PrimaryKeyID extracts a numeric primary key from a struct value.

func ResponsiveConversionKey

func ResponsiveConversionKey(conversionName string) string

ResponsiveConversionKey returns the JSON key used for a conversion (empty → original).

func ResponsiveImageFileName

func ResponsiveImageFileName(m Media, conversionName string, width, height int) string

ResponsiveImageFileName builds a Spatie-compatible responsive file name. Example: hero___media_library_original_640_360.jpg

func ResponsiveImagePath

func ResponsiveImagePath(m Media, fileName string) string

ResponsiveImagePath is the relative disk path for a responsive image file name.

func ResponsiveImagesDir

func ResponsiveImagesDir(m Media) string

ResponsiveImagesDir is the relative directory for responsive image files (Spatie layout).

Types

type AddOption

type AddOption func(*addOptions)

AddOption configures AddMedia.

func OnConversionsDisk

func OnConversionsDisk(name string) AddOption

OnConversionsDisk stores conversions/responsive files on the named disk.

func OnDisk

func OnDisk(name string) AddOption

OnDisk stores this media on the named disk (overrides collection default).

type CollectionBuilder

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

CollectionBuilder configures a collection.

func (*CollectionBuilder) AcceptsMimeTypes

func (b *CollectionBuilder) AcceptsMimeTypes(mimes ...string) *CollectionBuilder

AcceptsMimeTypes whitelists MIME types for uploads into this collection.

func (*CollectionBuilder) ConversionsDisk

func (b *CollectionBuilder) ConversionsDisk(name string) *CollectionBuilder

ConversionsDisk sets the disk for conversions and responsive images.

func (*CollectionBuilder) Disk

Disk sets the storage disk for originals in this collection.

func (*CollectionBuilder) SingleFile

func (b *CollectionBuilder) SingleFile() *CollectionBuilder

SingleFile keeps only the latest media item in the collection.

func (*CollectionBuilder) WithResponsiveImages

func (b *CollectionBuilder) WithResponsiveImages() *CollectionBuilder

WithResponsiveImages generates width variants for the original file in this collection.

type CollectionConfig

type CollectionConfig struct {
	Name             string
	SingleFile       bool
	AcceptsMimeTypes []string
	ResponsiveImages bool
	Disk             string // optional; empty → library default disk
	ConversionsDisk  string // optional; empty → same as Disk
}

CollectionConfig describes a named media collection on a model type.

type ConversionBuilder

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

ConversionBuilder configures an image conversion.

func (*ConversionBuilder) Crop

func (b *ConversionBuilder) Crop(width, height int) *ConversionBuilder

Crop resizes and center-crops to exact width x height.

func (*ConversionBuilder) Fit

func (b *ConversionBuilder) Fit(width, height int) *ConversionBuilder

Fit resizes to fit inside width x height preserving aspect ratio.

func (*ConversionBuilder) ForCollection

func (b *ConversionBuilder) ForCollection(name string) *ConversionBuilder

ForCollection scopes the conversion to media in collection.

func (*ConversionBuilder) Quality

func (b *ConversionBuilder) Quality(q int) *ConversionBuilder

Quality sets JPEG encode quality (1-100).

func (*ConversionBuilder) WithResponsiveImages

func (b *ConversionBuilder) WithResponsiveImages() *ConversionBuilder

WithResponsiveImages generates width variants for this conversion.

type ConversionConfig

type ConversionConfig struct {
	Name             string
	Collection       string
	Width            int
	Height           int
	Crop             bool
	Quality          int
	ResponsiveImages bool
}

ConversionConfig describes an image conversion.

type Disk

type Disk interface {
	Name() string
	Put(ctx context.Context, path string, r io.Reader) (int64, error)
	Delete(ctx context.Context, path string) error
	Exists(ctx context.Context, path string) (bool, error)
	Open(ctx context.Context, path string) (io.ReadCloser, error)
	// URL returns a publicly reachable URL for path (may be relative).
	URL(path string) string
}

Disk stores and serves media bytes. Implement this for S3, GCS, etc.

type FileSource

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

FileSource is a pending upload before it is attached to a model.

func FromPath

func FromPath(path string) (*FileSource, error)

FromPath opens a local file as a FileSource.

func FromReader

func FromReader(r io.Reader, fileName, mimeType string) (*FileSource, error)

FromReader wraps an in-memory or streamed upload.

func (*FileSource) WithName

func (f *FileSource) WithName(name string) *FileSource

WithName overrides the media "name" column (defaults to file basename without extension).

type HasMedia

type HasMedia interface {
	MediaModelID() uint64
	MediaModelType() string
}

HasMedia marks a model that can own media rows.

ModelType should be a stable morph alias (for example "seo_metas") when sharing a database with Laravel.

func ModelFrom

func ModelFrom(id uint64, modelType string) HasMedia

ModelFrom builds a HasMedia value from an explicit id and morph type.

type Library

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

Library is the entry point for media operations.

func New

func New(db *gorm.DB, opts ...Option) *Library

New creates a Library bound to db.

func (*Library) AddMedia

func (l *Library) AddMedia(ctx context.Context, model HasMedia, collectionName string, source *FileSource, opts ...AddOption) (*Media, error)

AddMedia attaches a file to model under collectionName.

Disk I/O (original, conversions, responsive) runs outside DB transactions so row locks are not held across local/S3 uploads. On failure after any disk writes, uploaded files and the media row are removed (compensating cleanup).

For singleFile collections, existing rows are locked during insert so concurrent uploads serialize when the collection is non-empty. Previous items are removed only after the new media is stored successfully; if clearing them fails, the new media is removed as well. A post-success pass also drops older stragglers (lower ID) left by empty-collection races.

Disk selection (highest priority wins): AddOption → collection Disk → library default.

Example
package main

import (
	"context"
	"fmt"
	"image"
	"image/color"
	"image/jpeg"
	"os"
	"path/filepath"

	medialibrary "github.com/noormaulida/gorm-medialibrary"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type examplePage struct {
	ID uint64
}

func (p examplePage) MediaModelID() uint64   { return p.ID }
func (p examplePage) MediaModelType() string { return "pages" }

func main() {
	db, _ := gorm.Open(sqlite.Open("file:example_media?mode=memory&cache=shared"), &gorm.Config{})
	_ = db.AutoMigrate(&examplePage{})
	_ = medialibrary.Migrate(db)

	root, _ := os.MkdirTemp("", "medialib-ex-")
	defer os.RemoveAll(root)
	disk := medialibrary.NewLocalDisk("public", root, "/storage")
	lib := medialibrary.New(db, medialibrary.WithDisk(disk))
	lib.Register("pages", func(r *medialibrary.Registrar) {
		r.Collection("cover").SingleFile()
		r.Conversion("thumb").ForCollection("cover").Fit(40, 40)
	})

	page := examplePage{ID: 1}
	_ = db.Create(&page)

	imgPath := filepath.Join(root, "in.jpg")
	f, _ := os.Create(imgPath)
	img := image.NewRGBA(image.Rect(0, 0, 80, 60))
	img.Set(0, 0, color.RGBA{R: 255, A: 255})
	_ = jpeg.Encode(f, img, nil)
	_ = f.Close()

	src, _ := medialibrary.FromPath(imgPath)
	media, _ := lib.AddMedia(context.Background(), page, "cover", src)
	url, _ := lib.GetURL(*media, "thumb")
	fmt.Println(media.CollectionName)
	fmt.Println(url != "")
}
Output:
cover
true

func (*Library) ClearMediaCollection

func (l *Library) ClearMediaCollection(ctx context.Context, model HasMedia, collectionName string) error

ClearMediaCollection deletes all media in a collection for model.

func (*Library) DB

func (l *Library) DB() *gorm.DB

DB exposes the underlying GORM handle.

func (*Library) Delete

func (l *Library) Delete(ctx context.Context, media Media) error

Delete removes media files from disk, then deletes the database row. Disk errors are returned (not swallowed). If a disk delete fails, the DB row is left intact so the operation can be retried.

func (*Library) Find

func (l *Library) Find(id uint64) (*Media, error)

Find finds media by primary key.

func (*Library) GetFirstMedia

func (l *Library) GetFirstMedia(model HasMedia, collectionName string) (*Media, error)

GetFirstMedia returns the first media item in a collection.

func (*Library) GetFirstMediaURL

func (l *Library) GetFirstMediaURL(model HasMedia, collectionName string, conversionName ...string) (string, error)

GetFirstMediaURL is a convenience wrapper around GetFirstMedia + GetURL.

func (*Library) GetMedia

func (l *Library) GetMedia(model HasMedia, collectionName ...string) ([]Media, error)

GetMedia returns media for model, optionally filtered by collection.

func (*Library) GetResponsiveImageURLs

func (l *Library) GetResponsiveImageURLs(media Media, conversionName ...string) ([]string, error)

GetResponsiveImageURLs returns public URLs for responsive variants of the original or a named conversion (Spatie getResponsiveImageUrls).

All responsive files (including media_library_original) are stored on the conversions disk when set, matching AddMedia write paths.

func (*Library) GetSrcset

func (l *Library) GetSrcset(media Media, conversionName ...string) (string, error)

GetSrcset builds an HTML srcset string for responsive images (Spatie getSrcset).

func (*Library) GetURL

func (l *Library) GetURL(media Media, conversionName ...string) (string, error)

GetURL returns a public URL for the original file or a named conversion.

func (*Library) Register

func (l *Library) Register(modelType string, fn func(*Registrar))

Register attaches collection/conversion definitions for a morph model type.

type LocalDisk

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

LocalDisk stores files under Root and exposes them under URLPrefix.

func NewLocalDisk

func NewLocalDisk(name, root, urlPrefix string) *LocalDisk

NewLocalDisk creates a filesystem disk.

NewLocalDisk("public", "./storage/app/public", "/storage")

func (*LocalDisk) Delete

func (d *LocalDisk) Delete(_ context.Context, path string) error

Delete implements Disk.

func (*LocalDisk) Exists

func (d *LocalDisk) Exists(_ context.Context, path string) (bool, error)

Exists implements Disk.

func (*LocalDisk) Name

func (d *LocalDisk) Name() string

Name implements Disk.

func (*LocalDisk) Open

func (d *LocalDisk) Open(_ context.Context, path string) (io.ReadCloser, error)

Open implements Disk.

func (*LocalDisk) Put

func (d *LocalDisk) Put(_ context.Context, path string, r io.Reader) (int64, error)

Put implements Disk.

func (*LocalDisk) Root

func (d *LocalDisk) Root() string

Root returns the filesystem root (useful in tests).

func (*LocalDisk) URL

func (d *LocalDisk) URL(path string) string

URL implements Disk.

type Media

type Media struct {
	ID                   uint64         `gorm:"primaryKey;autoIncrement"`
	ModelType            string         `gorm:"size:255;not null;index:media_model_type_model_id_index;column:model_type"`
	ModelID              uint64         `gorm:"not null;index:media_model_type_model_id_index;column:model_id"`
	UUID                 *string        `gorm:"type:char(36);uniqueIndex"`
	CollectionName       string         `gorm:"size:255;not null;column:collection_name"`
	Name                 string         `gorm:"size:255;not null"`
	FileName             string         `gorm:"size:255;not null;column:file_name"`
	MimeType             *string        `gorm:"size:255;column:mime_type"`
	Disk                 string         `gorm:"size:255;not null"`
	ConversionsDisk      *string        `gorm:"size:255;column:conversions_disk"`
	Size                 uint64         `gorm:"not null"`
	Manipulations        datatypes.JSON `gorm:"type:json;not null"`
	CustomProperties     datatypes.JSON `gorm:"type:json;not null;column:custom_properties"`
	GeneratedConversions datatypes.JSON `gorm:"type:json;not null;column:generated_conversions"`
	ResponsiveImages     datatypes.JSON `gorm:"type:json;not null;column:responsive_images"`
	OrderColumn          *int           `gorm:"column:order_column"`
	CreatedAt            *time.Time
	UpdatedAt            *time.Time
}

Media mirrors Spatie's media table.

func (Media) CustomPropertyMap

func (m Media) CustomPropertyMap() map[string]any

CustomPropertyMap decodes custom_properties JSON.

func (Media) GeneratedConversionMap

func (m Media) GeneratedConversionMap() map[string]bool

GeneratedConversionMap decodes generated_conversions JSON.

func (Media) ResponsiveImagesMap

func (m Media) ResponsiveImagesMap() ResponsiveImagesData

ResponsiveImagesMap decodes responsive_images JSON (Spatie object shape).

func (Media) TableName

func (Media) TableName() string

TableName keeps Spatie's default table name.

type Option

type Option func(*Library)

Option configures a Library.

func WithDefaultDisk

func WithDefaultDisk(name string) Option

WithDefaultDisk selects the disk name used when media.Disk is empty.

func WithDisk

func WithDisk(disk Disk) Option

WithDisk registers a storage disk.

type Registrar

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

Registrar collects collection and conversion definitions for a model type.

func (*Registrar) Collection

func (r *Registrar) Collection(name string) *CollectionBuilder

Collection starts (or returns) a collection definition.

func (*Registrar) Conversion

func (r *Registrar) Conversion(name string) *ConversionBuilder

Conversion starts a conversion definition.

type ResponsiveImageSet

type ResponsiveImageSet struct {
	URLs      []string `json:"urls"`
	Base64SVG string   `json:"base64svg,omitempty"`
}

ResponsiveImageSet holds file names and optional tiny placeholder for one conversion key.

type ResponsiveImagesData

type ResponsiveImagesData map[string]ResponsiveImageSet

ResponsiveImagesData is the Spatie-compatible responsive_images JSON shape.

type S3API

type S3API interface {
	PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
	GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
	DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
	HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
}

S3API is the subset of the AWS S3 client used by S3Disk. *s3.Client from aws-sdk-go-v2 satisfies this interface.

type S3Disk

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

S3Disk stores media objects in Amazon S3 (or compatible APIs).

func NewS3Disk

func NewS3Disk(cfg S3DiskConfig) (*S3Disk, error)

NewS3Disk creates an S3-backed Disk.

cfg, _ := config.LoadDefaultConfig(ctx)
client := s3.NewFromConfig(cfg)
disk, err := medialibrary.NewS3Disk(medialibrary.S3DiskConfig{
    Name: "s3", Bucket: "my-bucket", BaseURL: "https://cdn.example.com", Client: client,
})

func (*S3Disk) Delete

func (d *S3Disk) Delete(ctx context.Context, rel string) error

Delete implements Disk.

func (*S3Disk) Exists

func (d *S3Disk) Exists(ctx context.Context, rel string) (bool, error)

Exists implements Disk.

func (*S3Disk) Name

func (d *S3Disk) Name() string

Name implements Disk.

func (*S3Disk) Open

func (d *S3Disk) Open(ctx context.Context, rel string) (io.ReadCloser, error)

Open implements Disk.

func (*S3Disk) Put

func (d *S3Disk) Put(ctx context.Context, rel string, r io.Reader) (int64, error)

Put implements Disk.

func (*S3Disk) URL

func (d *S3Disk) URL(rel string) string

URL implements Disk.

type S3DiskConfig

type S3DiskConfig struct {
	// Name is the disk name stored on media rows (e.g. "s3").
	Name string
	// Bucket is the S3 bucket name.
	Bucket string
	// Prefix is an optional key prefix (without leading/trailing slashes).
	Prefix string
	// BaseURL is the public URL prefix for URL(), e.g.
	// "https://cdn.example.com" or "https://my-bucket.s3.amazonaws.com".
	BaseURL string
	// Client is an aws-sdk-go-v2 S3 client (or compatible mock).
	Client S3API
}

S3DiskConfig configures a built-in S3 (or S3-compatible) disk.

Jump to

Keyboard shortcuts

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