Documentation
¶
Overview ¶
Package shrine is a pure-Go (CGO-free) reimplementation of the deterministic core of Ruby's `shrine` gem — the file-attachment toolkit. It reproduces the pieces that need no Ruby runtime: the Storage abstraction and its built-in Memory / FileSystem backends, the named-storage registry, the Uploader (Shrine class) that extracts metadata and generates a location, the UploadedFile value with its `{"id","storage","metadata"}` JSON representation, and the Attacher cache→store promotion lifecycle.
It is the file-attachment layer for [go-embedded-ruby](https://github.com/go-embedded-ruby/ruby), but a standalone, reusable module.
What it is — and isn't ¶
Everything shrine does around the bytes is deterministic and needs no interpreter, so it lives here as pure Go: registering named storages, extracting the filename/size/mime_type metadata (mime by content sniffing via net/http.DetectContentType), generating a storage location (random id + extension by default), uploading to a storage, JSON-encoding/decoding the UploadedFile value, and driving the cache→store promotion lifecycle on the Attacher.
The pieces that touch the outside world are host seams, so tests stay hermetic:
- The filesystem Storage runs over an injectable FS seam. The default (OSFS) is backed by the os package; tests inject a fake to exercise the I/O error branches without a real disk.
- Location generation is the Shrine.GenerateLocation seam (default: random hex + extension); a test injects a deterministic generator.
- MIME detection is the Shrine.DetectMIME seam (default: net/http.DetectContentType).
Flow ¶
s := shrine.New()
s.Register("cache", shrine.NewMemory())
s.Register("store", shrine.NewMemory())
up, _ := s.Uploader("store")
file, _ := up.Upload(strings.NewReader("hello"), &shrine.UploadOptions{Filename: "a.txt"})
json, _ := file.ToJSON() // {"id":"….txt","storage":"store","metadata":{…}}
// rehydrate from the JSON representation
file2, _ := s.UploadedFile(json)
data, _ := file2.Download()
// attach lifecycle: assign to cache, finalize promotes to store
att, _ := s.Attacher("cache", "store")
att.Assign(strings.NewReader("hi"), &shrine.UploadOptions{Filename: "b.txt"})
att.Finalize()
Plugins ¶
The plugins real applications depend on are implemented here as pure Go. The class-level ones register through the Plugin seam (Shrine.Plugin, whose Configure hook receives the Shrine); the attachment-level ones are methods on Attacher/UploadedFile. Behaviour is matched against the shrine gem (v3.8) and pinned with differential test oracles.
- determine_mime_type — content-sniffed mime via a MIMEAnalyzer seam (DetermineMIMEType; default ContentAnalyzer over net/http, plus ExtensionAnalyzerFor).
- store_dimensions — image width/height via a DimensionAnalyzer (StoreDimensions; default ImageDimensions over the stdlib image decoders), read with UploadedFile.Width/UploadedFile.Height/ UploadedFile.Dimensions.
- add_metadata — pluggable [MetadataExtractor]s (Shrine.AddMetadata/ Shrine.AddMetadataKey); the foundation store_dimensions, signature and refresh_metadata build on.
- signature — md5/sha1/sha256/sha384/sha512/crc32 digests in hex/base64/raw (Signature), and SignatureMetadata to store one per upload.
- refresh_metadata — recompute metadata from the stored bytes (UploadedFile.RefreshMetadata).
- validation_helpers — size/extension/mime/dimension checks with the gem's messages (Validation), wired through Attacher.Validate.
- pretty_location — human-readable locations (Shrine.PrettyLocation).
- derivatives — process/store derived files (Attacher.CreateDerivatives, Attacher.AddDerivative, …), serialised in the column data.
- cached_attachment_data / restore_cached_data — Attacher.CachedData, Attacher.SetCached, Attacher.RestoreCachedData.
- data_uri — parse and attach "data:" URIs (DataURI, Attacher.AssignDataURI).
- remote_url — download and attach a URL through a Downloader seam (RemoteURL).
- upload_endpoint / presign_endpoint — [http.Handler]s (UploadEndpoint, PresignEndpoint over the Presigner seam).
- default_storage — default cache/store (DefaultStorage, Shrine.DefaultAttacher).
- activerecord / sequel — model attachment over the Record seam (ModelAttacher, Shrine.NewActiveRecord/Shrine.NewSequel); the save/destroy callback wiring is host-side.
Seams left to the host ¶
The parts that need infrastructure the pure-Go core does not carry stay injectable: storage backends beyond Memory/FileSystem (S3, GCS, …) are any Storage (and Presigner) implementation; the ORM row is the Record seam; the remote-url fetch is the Downloader seam; the mime and dimension analyzers are the MIMEAnalyzer/DimensionAnalyzer seams. Every test drives these through in-memory fakes, so the suite touches no network or disk.
Index ¶
- Variables
- func ContentAnalyzer(data []byte) string
- func DataURI(uri string) (mimeType string, data []byte, err error)
- func ImageDimensions(data []byte) (int, int, bool)
- func Signature(data []byte, algo SignatureAlgorithm, format SignatureFormat) (string, error)
- type Attacher
- func (a *Attacher) AddDerivative(name string, r io.Reader, opts *UploadOptions) error
- func (a *Attacher) Assign(r io.Reader, opts *UploadOptions) error
- func (a *Attacher) AssignDataURI(uri string, opts *UploadOptions) error
- func (a *Attacher) CachedData() (string, error)
- func (a *Attacher) Changed() bool
- func (a *Attacher) ColumnData() (string, error)
- func (a *Attacher) CreateDerivatives(deriver Deriver) error
- func (a *Attacher) DeleteDerivatives() error
- func (a *Attacher) Derivative(name string) *UploadedFile
- func (a *Attacher) DerivativeURL(name string, opts map[string]any) string
- func (a *Attacher) Derivatives() map[string]*UploadedFile
- func (a *Attacher) Destroy() error
- func (a *Attacher) Finalize() error
- func (a *Attacher) Get() *UploadedFile
- func (a *Attacher) LoadColumn(data string) error
- func (a *Attacher) Promote() error
- func (a *Attacher) RestoreCachedData(json string) error
- func (a *Attacher) Set(file *UploadedFile)
- func (a *Attacher) SetCached(json string) error
- func (a *Attacher) Valid() bool
- type DefaultStorage
- type Deriver
- type DetermineMIMEType
- type DimensionAnalyzer
- type Downloader
- type FS
- type FileSystem
- type LocationContext
- type MIMEAnalyzer
- type Memory
- type Metadata
- type MetadataExtractor
- type ModelAttacher
- type OSFS
- type Plugin
- type PresignData
- type PresignEndpoint
- type Presigner
- type Record
- type RemoteURL
- type Shrine
- func (s *Shrine) AddMetadata(ex MetadataExtractor)
- func (s *Shrine) AddMetadataKey(name string, fn func(data []byte, meta Metadata) any)
- func (s *Shrine) Attacher(cacheKey, storeKey string) (*Attacher, error)
- func (s *Shrine) DefaultAttacher() (*Attacher, error)
- func (s *Shrine) DefaultUploader() (*Uploader, error)
- func (s *Shrine) NewActiveRecord(cacheKey, storeKey string, record Record, column string) (*ModelAttacher, error)
- func (s *Shrine) NewSequel(cacheKey, storeKey string, record Record, column string) (*ModelAttacher, error)
- func (s *Shrine) Plugin(p Plugin)
- func (s *Shrine) Plugins() []Plugin
- func (s *Shrine) PrettyLocation(ctx LocationContext) string
- func (s *Shrine) Register(name string, st Storage)
- func (s *Shrine) Storages() map[string]Storage
- func (s *Shrine) UploadedFile(data string) (*UploadedFile, error)
- func (s *Shrine) Uploader(name string) (*Uploader, error)
- type SignatureAlgorithm
- type SignatureFormat
- type SignatureMetadata
- type Storage
- type StoreDimensions
- type UploadEndpoint
- type UploadOptions
- type UploadedFile
- func (f *UploadedFile) Data() map[string]any
- func (f *UploadedFile) Delete() error
- func (f *UploadedFile) Dimensions() ([2]int, bool)
- func (f *UploadedFile) Download() ([]byte, error)
- func (f *UploadedFile) Exists() bool
- func (f *UploadedFile) Filename() string
- func (f *UploadedFile) Height() int
- func (f *UploadedFile) MarshalJSON() ([]byte, error)
- func (f *UploadedFile) MimeType() string
- func (f *UploadedFile) Open() (io.ReadCloser, error)
- func (f *UploadedFile) RefreshMetadata() error
- func (f *UploadedFile) Replace(r io.Reader, opts *UploadOptions) (*UploadedFile, error)
- func (f *UploadedFile) Size() int64
- func (f *UploadedFile) Stream(w io.Writer) error
- func (f *UploadedFile) ToJSON() (string, error)
- func (f *UploadedFile) URL(options map[string]any) string
- func (f *UploadedFile) Width() int
- type Uploader
- type Validation
- func (v *Validation) Extension(allowed []string) bool
- func (v *Validation) MaxDimensions(max [2]int) (bool, error)
- func (v *Validation) MaxHeight(max int) (bool, error)
- func (v *Validation) MaxSize(max int64) bool
- func (v *Validation) MaxWidth(max int) (bool, error)
- func (v *Validation) MimeType(allowed []string) bool
- func (v *Validation) MinDimensions(min [2]int) (bool, error)
- func (v *Validation) MinHeight(min int) (bool, error)
- func (v *Validation) MinSize(min int64) bool
- func (v *Validation) MinWidth(min int) (bool, error)
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidDataURI = fmt.Errorf("shrine: invalid data URI")
ErrInvalidDataURI is returned (wrapped) by DataURI for a value that is not a parseable data URI, mirroring the data_uri plugin's `ParseError`.
var ErrMissingDimension = fmt.Errorf("shrine: dimension metadata is missing")
ErrMissingDimension is returned (wrapped) by the dimension validators when the file has no width/height metadata (store_dimensions was not run). It mirrors the gem raising `Shrine::Error "width metadata is missing"`.
var ErrNotCached = fmt.Errorf("shrine: file is not in the cache storage")
ErrNotCached is returned (wrapped) when a file submitted as a cached attachment does not live in the cache storage, mirroring the gem's `Shrine::Plugins::Cached...`/`NotCached` guard.
var ErrNotFound = errors.New("shrine: file not found")
ErrNotFound is returned (wrapped) when opening an id that does not exist in a storage.
var ErrRemoteURLDownload = fmt.Errorf("shrine: remote url download failed")
ErrRemoteURLDownload wraps a Downloader failure.
var ErrRemoteURLTooLarge = fmt.Errorf("shrine: remote url exceeds max size")
ErrRemoteURLTooLarge is returned when a downloaded body exceeds RemoteURL.MaxSize, mirroring the gem's `max_size` guard.
var ErrUnknownAlgorithm = fmt.Errorf("shrine: unknown signature algorithm")
ErrUnknownAlgorithm is returned (wrapped) by Signature for an unsupported algorithm, mirroring the gem raising on an unknown algorithm.
var ErrUnknownFormat = fmt.Errorf("shrine: unknown signature format")
ErrUnknownFormat is returned (wrapped) by Signature for an unsupported format.
var ErrUnknownStorage = errors.New("shrine: unknown storage")
ErrUnknownStorage is returned (wrapped) when a storage name is not present in the registry — for example when rehydrating an UploadedFile whose "storage" key is not registered, or when asking for an Uploader on an unknown name. It mirrors the gem's Shrine::Error "storage :name isn't registered".
Functions ¶
func ContentAnalyzer ¶
ContentAnalyzer detects the mime type from content via net/http.DetectContentType. It is the default analyzer and the analogue of the gem's content-based `:file`/`:marcel` analyzers.
Differential note vs the gem's default `:file` analyzer: DetectContentType appends a charset for text (e.g. "text/plain; charset=utf-8") and never returns the empty string — an empty or unrecognised file sniffs as "text/plain; charset=utf-8" or "application/octet-stream" rather than the gem's nil. The recognised binary signatures (PNG, JPEG, GIF, PDF, ZIP, …) agree with the gem.
func DataURI ¶
DataURI parses an RFC 2397 "data:" URI into its media type and decoded bytes, mirroring `Shrine.data_uri(uri)` from the data_uri plugin. The media type defaults to "text/plain" when omitted (keeping any parameters such as ";charset=utf-8"). A ";base64" payload is base64-decoded; otherwise it is form-unescaped (percent escapes decoded and "+" treated as space), matching the gem. It returns a wrapped ErrInvalidDataURI for a malformed URI or an undecodable payload.
func ImageDimensions ¶
ImageDimensions is the default DimensionAnalyzer: it reads the image header via image.DecodeConfig, recognising the formats whose decoders are registered (PNG, JPEG, GIF here). It returns ok=false for non-image or unrecognised data, mirroring the gem returning nil dimensions.
func Signature ¶
func Signature(data []byte, algo SignatureAlgorithm, format SignatureFormat) (string, error)
Signature computes the digest of data under algo and encodes it per format, mirroring `Shrine.signature(io, algorithm, format:)`.
The crc32 case reproduces the gem faithfully: its raw digest is the checksum rendered as a *decimal string* (e.g. "222957957"), so None returns that string and Hex/Base64 encode the bytes of that decimal string — not the four checksum bytes. The cryptographic algorithms digest to raw bytes as usual.
Types ¶
type Attacher ¶
type Attacher struct {
// Validate, when set, runs on every attachment change and returns the
// validation error messages, which are recorded in [Attacher.Errors].
// It is the seam the validation_helpers plugin plugs into (the gem's
// `Attacher.validate do … end` block); build the messages with a
// [Validation]. See [Attacher.Valid].
Validate func(file *UploadedFile) []string
// Errors holds the messages from the last [Attacher.Validate] run,
// mirroring `Attacher#errors`.
Errors []string
// contains filtered or unexported fields
}
Attacher drives the cache→store attachment lifecycle, mirroring Shrine::Attacher. Newly assigned files land in the cache storage; finalizing promotes them to the permanent store storage and deletes the previously stored file (replacement). It tracks whether the attachment has changed.
func (*Attacher) AddDerivative ¶
AddDerivative uploads r to the store storage and records it as the named derivative, mirroring `Attacher#add_derivative(name, io)`. It overwrites any existing derivative of the same name (the caller is responsible for deleting the replaced file if desired).
func (*Attacher) Assign ¶
func (a *Attacher) Assign(r io.Reader, opts *UploadOptions) error
Assign uploads r to the cache storage and sets it as the (changed) current attachment, mirroring `Attacher#assign(io)` / `#attach_cached`.
func (*Attacher) AssignDataURI ¶
func (a *Attacher) AssignDataURI(uri string, opts *UploadOptions) error
AssignDataURI parses a "data:" URI, uploads its bytes to the cache storage and sets the result as the (changed) attachment, mirroring `Attacher#assign_data_uri`. The parsed media type seeds the "mime_type" metadata override (so it is not re-sniffed), matching the gem. It returns a wrapped ErrInvalidDataURI on a bad URI or the upload error otherwise.
func (*Attacher) CachedData ¶
CachedData returns the JSON representation of the current attachment when it is a cached file, to be round-tripped through a hidden form field so a failed form submission can retain the upload. It returns ("", nil) when nothing is attached or the attachment is already stored. It mirrors the cached_attachment_data plugin's `Attacher#cached_data`.
func (*Attacher) Changed ¶
Changed reports whether the attachment differs from the last finalized state, mirroring `Attacher#changed?`.
func (*Attacher) ColumnData ¶
ColumnData serialises the current attachment (and any derivatives) to the JSON stored in a model's attachment column, mirroring `Attacher#column_data`. It returns ("", nil) when nothing is attached, so the host writes SQL NULL.
func (*Attacher) CreateDerivatives ¶
CreateDerivatives runs deriver against the current attachment's bytes and uploads every produced file to the store storage as a named derivative, mirroring `Attacher#create_derivatives`. It is a no-op with no error when nothing is attached (the gem raises; here the absence is reported by the returned files being unchanged).
func (*Attacher) DeleteDerivatives ¶
DeleteDerivatives deletes every recorded derivative from storage and clears the set, mirroring `Attacher#delete_derivatives`. It stops and returns on the first delete error.
func (*Attacher) Derivative ¶
func (a *Attacher) Derivative(name string) *UploadedFile
Derivative returns the named derivative, or nil if absent, mirroring `Attacher#derivatives[name]`.
func (*Attacher) DerivativeURL ¶
DerivativeURL returns the URL of the named derivative, or "" if absent, mirroring `Attacher#url(name)` from the derivatives plugin.
func (*Attacher) Derivatives ¶
func (a *Attacher) Derivatives() map[string]*UploadedFile
Derivatives returns the recorded derivatives keyed by name (nil if none), mirroring `Attacher#derivatives`.
func (*Attacher) Finalize ¶
Finalize commits a change: it promotes a cached attachment to the store, deletes the previously finalized file (replacement) and clears the changed flag. A no-op when nothing changed. Mirrors `Attacher#finalize`.
func (*Attacher) Get ¶
func (a *Attacher) Get() *UploadedFile
Get returns the current attachment, or nil when nothing is attached. Mirrors `Attacher#get`/`#file`.
func (*Attacher) LoadColumn ¶
LoadColumn replaces the attacher's state from column JSON produced by Attacher.ColumnData (or the gem), binding the attached file and its derivatives to the registry and marking the attacher unchanged (this is a load, not an assignment, so no validation runs). An empty string clears the attachment. It mirrors `Attacher#load_column`.
func (*Attacher) Promote ¶
Promote uploads the current cached attachment to the store storage and makes the stored copy the current attachment. A no-op if nothing is attached. Mirrors `Attacher#promote`.
func (*Attacher) RestoreCachedData ¶
RestoreCachedData rehydrates a cached file from its JSON, recomputes its metadata from the actual stored bytes (discarding any client-forged values), then sets it as the attachment. It mirrors the restore_cached_data plugin layered on cached_attachment_data. It returns ErrNotCached for a non-cache file and propagates a [RefreshMetadata] error (e.g. the bytes are gone).
func (*Attacher) Set ¶
func (a *Attacher) Set(file *UploadedFile)
Set makes file the current (changed) attachment without uploading — for an already-uploaded file (e.g. a cached file submitted by a form) or nil to detach. Mirrors `Attacher#set`.
func (*Attacher) SetCached ¶
SetCached rehydrates a cached file from the JSON produced by [CachedData] and sets it as the (changed) attachment, without re-reading its bytes. It mirrors the plain cached_attachment_data assignment: the client-supplied metadata is trusted as-is. Use [RestoreCachedData] instead when the metadata must be recomputed from the stored bytes. It returns ErrNotCached if the rehydrated file is not in the cache storage.
type DefaultStorage ¶
type DefaultStorage struct {
// Cache and Store are the default storage names.
Cache string
Store string
}
DefaultStorage is the default_storage plugin: it records the cache and store storage names so attachers can be created without naming them each time, via Shrine.DefaultAttacher and Shrine.DefaultUploader.
s.Plugin(&shrine.DefaultStorage{Cache: "cache", Store: "store"})
att, _ := s.DefaultAttacher()
func (*DefaultStorage) Configure ¶
func (p *DefaultStorage) Configure(s *Shrine)
Configure records the default cache/store names on s.
type Deriver ¶
Deriver produces derived files (e.g. thumbnails, transcodes) from the bytes of an original, keyed by name, mirroring a block registered with the gem's `derivatives_processor`. The returned readers are uploaded to the store storage by Attacher.CreateDerivatives.
type DetermineMIMEType ¶
type DetermineMIMEType struct {
// Analyzer overrides the mime seam; nil means [ContentAnalyzer].
Analyzer MIMEAnalyzer
}
DetermineMIMEType is the determine_mime_type plugin: it makes uploads sniff the mime type from the file's content (not a client-supplied header) via the configured MIMEAnalyzer. A zero Analyzer uses ContentAnalyzer.
s.Plugin(&shrine.DetermineMIMEType{}) // stdlib content sniffing
func (*DetermineMIMEType) Configure ¶
func (p *DetermineMIMEType) Configure(s *Shrine)
Configure installs the analyzer as Shrine.DetectMIME.
type DimensionAnalyzer ¶
DimensionAnalyzer reports the pixel width and height of an image from its bytes, and whether they could be determined. It is the seam the store_dimensions plugin uses — the analogue of the gem's `:analyzer` option (`:fastimage`, `:mini_magick`, `:ruby_vips`, …).
type Downloader ¶
type Downloader func(url string) (body io.ReadCloser, filename string, err error)
Downloader fetches the bytes at a remote URL, returning a reader over the body (the caller closes it) and a suggested filename (may be ""). It is the seam the remote_url plugin downloads through — the pure-Go analogue of the gem's `down` dependency — so tests inject a fake and never touch the network.
func HTTPDownloader ¶
func HTTPDownloader(client *http.Client) Downloader
HTTPDownloader returns a Downloader that GETs the URL with client (or http.DefaultClient when nil) and derives the filename from the URL path. A non-2xx response is an error. The client's transport is the injection point: tests supply a canned http.RoundTripper, so no socket is opened.
type FS ¶
type FS interface {
// MkdirAll creates dir and any missing parents.
MkdirAll(dir string, perm os.FileMode) error
// WriteFile writes data to path, truncating an existing file.
WriteFile(path string, data []byte, perm os.FileMode) error
// ReadFile returns the contents of path.
ReadFile(path string) ([]byte, error)
// Remove deletes path.
Remove(path string) error
// Exists reports whether path exists.
Exists(path string) bool
}
FS is the filesystem seam FileSystem runs over. The default implementation (OSFS) is backed by the os package; tests inject a fake to exercise the I/O error branches hermetically.
type FileSystem ¶
type FileSystem struct {
// Directory is the base directory ids are stored under.
Directory string
// Prefix, if set, is prepended (with "/") to ids in URL. When empty, URL
// returns the on-disk path.
Prefix string
// DirPerm and FilePerm are the permissions for created directories/files.
DirPerm os.FileMode
FilePerm os.FileMode
// contains filtered or unexported fields
}
FileSystem is a Storage that persists files under a base Directory, mirroring Shrine::Storage::FileSystem. All I/O goes through the FS seam (default OSFS).
func NewFileSystem ¶
func NewFileSystem(directory string) *FileSystem
NewFileSystem returns a filesystem storage rooted at directory, using the production OSFS seam and 0755/0644 permissions.
func NewFileSystemWithFS ¶
func NewFileSystemWithFS(directory string, fs FS) *FileSystem
NewFileSystemWithFS returns a filesystem storage using a custom FS seam (used by tests to inject failures).
func (*FileSystem) Delete ¶
func (s *FileSystem) Delete(id string) error
Delete removes the file for id.
func (*FileSystem) Exists ¶
func (s *FileSystem) Exists(id string) bool
Exists reports whether the file for id exists.
func (*FileSystem) Open ¶
func (s *FileSystem) Open(id string) (io.ReadCloser, error)
Open returns a reader over the file for id, or a wrapped ErrNotFound.
type LocationContext ¶
type LocationContext struct {
// Namespace is the record's class location, e.g. "photo" or "user/avatar".
Namespace string
// Identifier is the record's primary key rendered as a string, e.g. "42".
Identifier string
// Name is the attachment name, e.g. "image".
Name string
// Metadata seeds the trailing basename's extension (from "filename").
Metadata Metadata
}
LocationContext carries the record context the pretty_location plugin folds into a human-readable storage location. In the gem this context comes from the model attachment (record class, record id, attachment name); here the host supplies it, because the pure-Go core has no ORM. Any empty field is omitted (the gem's `compact`).
type MIMEAnalyzer ¶
MIMEAnalyzer sniffs a mime type from the leading bytes of a file. It is the seam the determine_mime_type plugin swaps in for Shrine.DetectMIME — the analogue of the gem's `:analyzer` option (`:file`, `:marcel`, `:mimemagic`, `:content_type`, …). Two pure-Go analyzers ship here.
func ExtensionAnalyzerFor ¶
func ExtensionAnalyzerFor(filename string) MIMEAnalyzer
ExtensionAnalyzerFor returns a MIMEAnalyzer that maps the given filename's extension to a mime type via mime.TypeByExtension, falling back to ContentAnalyzer when the extension is unknown. It mirrors the gem's `:mini_mime` (extension-based) analyzer. The analyzer ignores its byte argument for the extension lookup, so the filename is captured here.
type Memory ¶
type Memory struct {
// contains filtered or unexported fields
}
Memory is an in-process Storage backed by a map, mirroring Shrine::Storage::Memory. It is handy for tests and ephemeral caches. URLs are of the form "memory://<id>".
func (*Memory) Open ¶
func (m *Memory) Open(id string) (io.ReadCloser, error)
Open returns a reader over the bytes stored at id, or a wrapped ErrNotFound.
type Metadata ¶
Metadata is the open metadata hash carried by an UploadedFile, mirroring the gem's Shrine metadata Hash. The three core keys shrine extracts are "filename", "size" and "mime_type"; plugins (and callers) may add more. Size is stored as an int64 so it round-trips through JSON as a number.
func (Metadata) Int ¶
Int returns the integer value stored under key, or 0 if absent/null or not a number. It accepts the int produced by an extractor and the float64 produced by a JSON round-trip.
func (Metadata) Size ¶
Size returns the "size" value in bytes, or 0 if absent/null. It accepts both the int64 produced on upload and the float64 produced by JSON decoding.
func (Metadata) String ¶
String returns the string value stored under key, or "" if absent/null or not a string. It is the generic accessor add_metadata-defined keys are read through (the gem defines an `UploadedFile#<key>` reader; Go cannot add methods dynamically, so callers read named keys through these helpers).
type MetadataExtractor ¶
MetadataExtractor computes extra metadata from an upload, mirroring a block registered with the gem's `add_metadata` plugin. It receives the raw content and the metadata accumulated so far (the core filename/size/mime_type plus any earlier extractors) and returns key/value pairs to merge in. Returning nil (or an empty map) adds nothing.
Extractors run during every upload, in registration order, after the core keys and before the caller's explicit `metadata:` overrides — exactly where the gem runs `add_metadata` blocks.
type ModelAttacher ¶
type ModelAttacher struct {
*Attacher
// contains filtered or unexported fields
}
ModelAttacher binds an Attacher to a Record column, mirroring the model attachment the activerecord/sequel plugins install. It reads the column on load, promotes on save and clears on destroy, persisting the serialized data (including derivatives) back through the Record seam.
func (*ModelAttacher) Destroy ¶
func (m *ModelAttacher) Destroy() error
Destroy deletes the attachment (and its derivatives) and clears the record's column, mirroring the gem's after-destroy callback.
func (*ModelAttacher) Load ¶
func (m *ModelAttacher) Load() error
Load reads the record's attachment column into the attacher, mirroring the gem loading the attachment when a record is instantiated.
func (*ModelAttacher) Save ¶
func (m *ModelAttacher) Save() error
Save finalizes the attachment (promotes a cached file to store, deletes any replaced file) and writes the new serialized column value back to the record, mirroring the gem's before/after-save callbacks. It is a no-op when nothing changed.
type OSFS ¶
type OSFS struct{}
OSFS is the production FS, backed by the os package.
type Plugin ¶
type Plugin interface {
Configure(s *Shrine)
}
Plugin is the minimal plugin-registration seam. Configure is called with the Shrine when the plugin is registered via Shrine.Plugin. The gem's wider plugin set is out of scope; this is the extension point a host wires into.
type PresignData ¶
type PresignData struct {
Method string `json:"method"`
URL string `json:"url"`
Fields map[string]string `json:"fields"`
Headers map[string]string `json:"headers"`
}
PresignData is the direct-upload descriptor a Presigner returns and the presign_endpoint serialises, mirroring the gem's presign response ({method, url, fields, headers}).
type PresignEndpoint ¶
type PresignEndpoint struct {
// Presigner authorises the upload; required.
Presigner Presigner
// GenerateID produces the storage location to presign; nil means a random
// hex id.
GenerateID func() string
// Options are passed through to [Presigner.Presign]; may be nil.
Options map[string]any
}
PresignEndpoint is the presign_endpoint plugin: an http.Handler that generates a storage location, asks a Presigner to authorise a direct upload to it and returns the PresignData as JSON. The presigning storage is the seam; this endpoint has no built-in backend.
Responses: 200 with the PresignData JSON on success; 405 for a non-GET method; 500 when presigning fails.
func (*PresignEndpoint) ServeHTTP ¶
func (e *PresignEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler.
type Presigner ¶
type Presigner interface {
// Presign authorises an upload to id and returns the descriptor a client
// posts the bytes with. opts are backend-specific and may be nil.
Presign(id string, opts map[string]any) (PresignData, error)
}
Presigner is a storage capable of authorising a direct client upload — the seam the presign_endpoint depends on. The built-in Memory/FileSystem storages cannot presign; an S3-style backend (added by the host) satisfies this.
type Record ¶
type Record interface {
// ReadAttachment returns the serialized column value and whether it is
// present/non-null.
ReadAttachment(column string) (value string, present bool)
// WriteAttachment stores the serialized column value ("" for SQL NULL).
WriteAttachment(column string, value string)
}
Record is the ORM-row seam the activerecord and sequel glue plug into. It abstracts reading and writing a single attachment column on a persisted record. The pure-Go core has no ORM, so the host adapts its model rows to this interface; the actual save/destroy *callbacks* (which is all that distinguishes the gem's `activerecord` and `sequel` plugins) are wired host-side around ModelAttacher.Save / ModelAttacher.Destroy.
type RemoteURL ¶
type RemoteURL struct {
// Download fetches the bytes; required.
Download Downloader
// MaxSize caps the downloaded size in bytes; 0 means unlimited. A body
// larger than MaxSize yields [ErrRemoteURLTooLarge].
MaxSize int64
}
RemoteURL is the remote_url plugin: it downloads a file from a URL through a Downloader seam and attaches it to the cache storage.
func (*RemoteURL) AssignTo ¶
func (r *RemoteURL) AssignTo(a *Attacher, url string, opts *UploadOptions) error
AssignTo downloads url and sets the result as a's (changed) cached attachment, mirroring `Attacher#assign_remote_url`. The downloaded filename seeds the "filename" metadata unless opts already carries one. A download failure is wrapped as ErrRemoteURLDownload; an over-large body is ErrRemoteURLTooLarge.
type Shrine ¶
type Shrine struct {
// GenerateLocation produces the storage id for an upload from its metadata.
// The default is a random hex string plus the filename's extension. Replace
// it to make locations deterministic (tests) or content-addressed.
GenerateLocation func(meta Metadata) string
// DetectMIME sniffs the mime type of the leading bytes of a file. The
// default is net/http.DetectContentType.
DetectMIME func(data []byte) string
// contains filtered or unexported fields
}
Shrine is a configured attachment context: the named-storage registry plus the location and MIME seams. It plays the role of the gem's Shrine class (which holds `Shrine.storages` and the uploader behaviour).
func (*Shrine) AddMetadata ¶
func (s *Shrine) AddMetadata(ex MetadataExtractor)
AddMetadata registers a multi-key MetadataExtractor, mirroring the gem's block form `add_metadata { |io, **| { … } }`.
func (*Shrine) AddMetadataKey ¶
AddMetadataKey registers a single-key extractor, mirroring the gem's `add_metadata(:key) { |io, **| … }`. A nil return from fn stores a null value under name, matching the gem (the key is always present once registered).
func (*Shrine) Attacher ¶
Attacher returns an Attacher whose cache and store uploaders are bound to the named storages, or a wrapped ErrUnknownStorage if either is missing.
func (*Shrine) DefaultAttacher ¶
DefaultAttacher returns an Attacher over the storages configured by the default_storage plugin, or a wrapped ErrUnknownStorage if either default is unset or unregistered.
func (*Shrine) DefaultUploader ¶
DefaultUploader returns an Uploader over the default store storage, mirroring uploading to the default storage without naming it.
func (*Shrine) NewActiveRecord ¶
func (s *Shrine) NewActiveRecord(cacheKey, storeKey string, record Record, column string) (*ModelAttacher, error)
NewActiveRecord binds a ModelAttacher for record's column, using the named cache/store storages, and loads the current column value. It is the activerecord plugin's model attachment; the difference from [NewSequel] is only the host-side callback wiring. It returns a wrapped ErrUnknownStorage if a storage is missing, or a load error for a malformed column value.
func (*Shrine) NewSequel ¶
func (s *Shrine) NewSequel(cacheKey, storeKey string, record Record, column string) (*ModelAttacher, error)
NewSequel is the sequel plugin's model attachment. It is identical to Shrine.NewActiveRecord at the pure-Go layer (see Record).
func (*Shrine) PrettyLocation ¶
func (s *Shrine) PrettyLocation(ctx LocationContext) string
PrettyLocation builds a human-readable storage location of the form "namespace/identifier/name/basename.ext", mirroring the pretty_location plugin. The basename (and its extension) come from Shrine.GenerateLocation, so the default random-hex-plus-extension basename is preserved and tests can inject a deterministic generator. Empty context fields are dropped, so a contextless call returns just the basename — identical to the plain generator.
Pass the result as UploadOptions.Location:
loc := s.PrettyLocation(shrine.LocationContext{
Namespace: "photo", Identifier: "42", Name: "image",
Metadata: shrine.Metadata{"filename": "me.jpg"},
})
up.Upload(r, &shrine.UploadOptions{Location: loc}) // photo/42/image/<hex>.jpg
func (*Shrine) Register ¶
Register adds st to the registry under name (e.g. "cache" or "store"), mirroring assigning to `Shrine.storages[:name]`.
func (*Shrine) UploadedFile ¶
func (s *Shrine) UploadedFile(data string) (*UploadedFile, error)
UploadedFile rehydrates an UploadedFile from its JSON representation (`{"id","storage","metadata"}`), binding it to this Shrine so its storage can be resolved. It returns a wrapped ErrUnknownStorage if the "storage" name is not registered. Mirrors `Shrine.uploaded_file(data)`.
type SignatureAlgorithm ¶
type SignatureAlgorithm string
SignatureAlgorithm selects the digest function for Signature, mirroring the gem's signature plugin `algorithm` argument.
const ( MD5 SignatureAlgorithm = "md5" SHA1 SignatureAlgorithm = "sha1" SHA256 SignatureAlgorithm = "sha256" SHA384 SignatureAlgorithm = "sha384" SHA512 SignatureAlgorithm = "sha512" CRC32 SignatureAlgorithm = "crc32" )
The algorithms the gem's signature plugin supports.
type SignatureFormat ¶
type SignatureFormat string
SignatureFormat selects the encoding of the raw digest, mirroring the gem's `format:` option.
const ( // Hex hex-encodes the digest (the gem's default). Hex SignatureFormat = "hex" // Base64 base64-encodes the digest (standard padded alphabet). Base64 SignatureFormat = "base64" // None returns the raw digest bytes as a string. None SignatureFormat = "none" )
The output encodings the gem's signature plugin supports.
type SignatureMetadata ¶
type SignatureMetadata struct {
// Key is the metadata key to store the signature under (e.g. "md5").
Key string
// Algorithm and Format select the digest and its encoding; a zero Format
// means [Hex].
Algorithm SignatureAlgorithm
Format SignatureFormat
}
SignatureMetadata is the signature plugin composed with add_metadata: it registers an extractor that stores the digest of every upload under Key. It is the Go analogue of the common `add_metadata(:md5) { signature(io, :md5) }` recipe. Enable it with Shrine.Plugin:
s.Plugin(&shrine.SignatureMetadata{Key: "md5", Algorithm: shrine.MD5, Format: shrine.Hex})
func (*SignatureMetadata) Configure ¶
func (p *SignatureMetadata) Configure(s *Shrine)
Configure registers the signing MetadataExtractor. An unsupported algorithm/format yields a null value under Key rather than failing the upload, so a misconfiguration is visible in the metadata without aborting.
type Storage ¶
type Storage interface {
// Upload persists the bytes read from r under id. meta carries the
// extracted shrine metadata (advisory; backends may ignore it).
Upload(r io.Reader, id string, meta map[string]any) error
// Open returns a reader over the stored bytes, or a wrapped [ErrNotFound]
// if id is absent. The caller closes it.
Open(id string) (io.ReadCloser, error)
// Exists reports whether id is present.
Exists(id string) bool
// Delete removes id. Deleting an absent id is not an error.
Delete(id string) error
// URL returns a locator for id. options are backend-specific and may be nil.
URL(id string, options map[string]any) string
}
Storage is the abstract file backend, mirroring Shrine::Storage. A concrete storage persists opaque bytes under a caller-chosen id (the "location"). The package ships two implementations, Memory and FileSystem; a host can add its own (S3, GCS, …) by satisfying this interface.
This maps to the gem's storage contract:
upload(io, id, shrine_metadata:) -> Upload(r, id, meta) open(id) -> Open(id) exists?(id) -> Exists(id) delete(id) -> Delete(id) url(id, **options) -> URL(id, options)
type StoreDimensions ¶
type StoreDimensions struct {
// Analyzer overrides the dimension analyzer; nil means [ImageDimensions].
Analyzer DimensionAnalyzer
}
StoreDimensions is the store_dimensions plugin: it adds "width" and "height" metadata to every upload of an image, and returns null for both on non-images — matching the gem, which always defines the keys. A zero Analyzer uses ImageDimensions.
s.Plugin(&shrine.StoreDimensions{})
// later: f.Width(), f.Height(), f.Dimensions()
func (*StoreDimensions) Configure ¶
func (p *StoreDimensions) Configure(s *Shrine)
Configure registers a MetadataExtractor that stores width/height.
type UploadEndpoint ¶
type UploadEndpoint struct {
// Shrine is the attachment context; required.
Shrine *Shrine
// StorageKey names the storage uploads land in (e.g. "cache"); required.
StorageKey string
// FieldName is the multipart field the file arrives in; "" means "file".
FieldName string
// MaxMemory bounds the in-memory multipart buffer in bytes; 0 uses the
// net/http default (32 MiB).
MaxMemory int64
}
UploadEndpoint is the upload_endpoint plugin: an http.Handler that accepts a multipart POST, uploads the "file" part to a named storage (typically the cache) and returns the resulting UploadedFile as JSON. It is the Go analogue of the gem's Rack app; mount it under any router.
Responses: 200 with the UploadedFile JSON on success; 405 for a non-POST method; 400 when the "file" part is missing; 500 when the storage is unknown or the upload fails.
func (*UploadEndpoint) ServeHTTP ¶
func (e *UploadEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler.
type UploadOptions ¶
type UploadOptions struct {
// Location overrides the generated storage id when non-empty
// (the gem's `location:` option).
Location string
// Filename seeds the extracted "filename" metadata (the gem reads
// io.original_filename); ignored if Metadata already carries "filename".
Filename string
// Metadata overrides/augments the extracted metadata (the gem's
// `metadata:` option). Keys present here win over extraction.
Metadata Metadata
}
UploadOptions tunes an Uploader.Upload call, mirroring the keyword options of the gem's `Shrine#upload`.
type UploadedFile ¶
type UploadedFile struct {
// ID is the storage location (Shrine's "id").
ID string
// StorageKey is the name of the storage holding the file (Shrine's
// "storage").
StorageKey string
// Metadata is the extracted metadata hash.
Metadata Metadata
// contains filtered or unexported fields
}
UploadedFile is the value returned by an upload and the reference persisted in place of a file, mirroring Shrine::UploadedFile. It carries the storage id, the storage name and the extracted Metadata, and knows how to reach its bytes through the bound Shrine's registry.
func (*UploadedFile) Data ¶
func (f *UploadedFile) Data() map[string]any
Data returns the plain representation of the file — the Ruby Hash that `Shrine::UploadedFile#data` returns: {"id" => …, "storage" => …, "metadata" => {…}}.
func (*UploadedFile) Delete ¶
func (f *UploadedFile) Delete() error
Delete removes the file from its storage, mirroring `Shrine::UploadedFile#delete`.
func (*UploadedFile) Dimensions ¶
func (f *UploadedFile) Dimensions() ([2]int, bool)
Dimensions returns the [width, height] pair and whether both are present, mirroring `UploadedFile#dimensions` (which returns nil when either is missing).
func (*UploadedFile) Download ¶
func (f *UploadedFile) Download() ([]byte, error)
Download reads and returns all of the file's bytes, mirroring `Shrine::UploadedFile#download`.
func (*UploadedFile) Exists ¶
func (f *UploadedFile) Exists() bool
Exists reports whether the file is present in its storage, mirroring `Shrine::UploadedFile#exists?`.
func (*UploadedFile) Filename ¶
func (f *UploadedFile) Filename() string
Filename returns the "filename" metadata.
func (*UploadedFile) Height ¶
func (f *UploadedFile) Height() int
Height returns the "height" metadata in pixels (`UploadedFile#height`), or 0 if absent/null.
func (*UploadedFile) MarshalJSON ¶
func (f *UploadedFile) MarshalJSON() ([]byte, error)
MarshalJSON encodes the file in the gem's `{"id","storage","metadata"}` shape.
func (*UploadedFile) MimeType ¶
func (f *UploadedFile) MimeType() string
MimeType returns the "mime_type" metadata.
func (*UploadedFile) Open ¶
func (f *UploadedFile) Open() (io.ReadCloser, error)
Open returns a reader over the file's bytes (the caller closes it), mirroring `Shrine::UploadedFile#open`/`#to_io`.
func (*UploadedFile) RefreshMetadata ¶
func (f *UploadedFile) RefreshMetadata() error
RefreshMetadata re-reads the file's bytes and recomputes its metadata in place, mirroring `Shrine::UploadedFile#refresh_metadata!` (the refresh_metadata plugin). Size and mime_type are re-sniffed from the actual content and every registered add_metadata extractor (e.g. store_dimensions' width/height, signature's digest) is re-run; the existing filename is preserved (it is context-derived, not present in the bytes), and any custom keys not recomputed are kept.
It is the trust boundary for client-supplied cached files: recomputing the metadata from the stored bytes discards any values a client may have forged.
func (*UploadedFile) Replace ¶
func (f *UploadedFile) Replace(r io.Reader, opts *UploadOptions) (*UploadedFile, error)
Replace uploads new bytes to the same storage, deletes this (now stale) file and returns the new UploadedFile. It mirrors the gem's replace semantics (upload the replacement, delete the replaced) at the file level.
func (*UploadedFile) Size ¶
func (f *UploadedFile) Size() int64
Size returns the "size" metadata in bytes.
func (*UploadedFile) Stream ¶
func (f *UploadedFile) Stream(w io.Writer) error
Stream copies the file's bytes to w, mirroring `Shrine::UploadedFile#stream`.
func (*UploadedFile) ToJSON ¶
func (f *UploadedFile) ToJSON() (string, error)
ToJSON returns the JSON representation, mirroring `Shrine::UploadedFile#to_json`.
func (*UploadedFile) URL ¶
func (f *UploadedFile) URL(options map[string]any) string
URL returns a locator for the file, or "" if its storage is unknown. Mirrors `Shrine::UploadedFile#url`.
func (*UploadedFile) Width ¶
func (f *UploadedFile) Width() int
Width returns the "width" metadata in pixels (the store_dimensions plugin's `UploadedFile#width`), or 0 if absent/null.
type Uploader ¶
type Uploader struct {
// contains filtered or unexported fields
}
Uploader uploads to a single named storage, mirroring an instance of the Shrine class bound to a storage key (e.g. `Shrine.new(:store)`).
func (*Uploader) StorageKey ¶
StorageKey returns the storage name this uploader is bound to.
func (*Uploader) Upload ¶
func (u *Uploader) Upload(r io.Reader, opts *UploadOptions) (*UploadedFile, error)
Upload extracts metadata from the bytes read from r, generates a location, stores the bytes and returns the resulting UploadedFile. Mirrors `Shrine#upload(io, **options)`.
type Validation ¶
type Validation struct {
Errors []string
// contains filtered or unexported fields
}
Validation accumulates validation error messages for one UploadedFile, mirroring the validation_helpers plugin's DSL. Each check appends a gem-identical message to Validation.Errors and returns whether the file passed that check. Wire the collected Errors back through Attacher.Validate:
att.Validate = func(f *shrine.UploadedFile) []string {
v := shrine.NewValidation(f)
v.MaxSize(5 << 20)
v.Extension([]string{"jpg", "png"})
v.MimeType([]string{"image/jpeg", "image/png"})
return v.Errors
}
func NewValidation ¶
func NewValidation(f *UploadedFile) *Validation
NewValidation returns a Validation bound to f.
func (*Validation) Extension ¶
func (v *Validation) Extension(allowed []string) bool
Extension validates that the filename's extension is one of allowed (compared case-insensitively), mirroring `validate_extension`. A file with no extension (including a null filename) fails.
func (*Validation) MaxDimensions ¶
func (v *Validation) MaxDimensions(max [2]int) (bool, error)
MaxDimensions validates width and height against the [w, h] ceiling, mirroring `validate_max_dimensions`. It errors if either dimension is missing.
func (*Validation) MaxHeight ¶
func (v *Validation) MaxHeight(max int) (bool, error)
MaxHeight validates that the image is at most max pixels tall, mirroring `validate_max_height`.
func (*Validation) MaxSize ¶
func (v *Validation) MaxSize(max int64) bool
MaxSize validates that the file is at most max bytes, mirroring `validate_max_size`. The message reports the limit in human units.
func (*Validation) MaxWidth ¶
func (v *Validation) MaxWidth(max int) (bool, error)
MaxWidth validates that the image is at most max pixels wide, mirroring `validate_max_width`. It errors if width metadata is missing.
func (*Validation) MimeType ¶
func (v *Validation) MimeType(allowed []string) bool
MimeType validates that the mime_type metadata is one of allowed (compared case-sensitively, as the gem does), mirroring `validate_mime_type`. A null mime_type fails.
func (*Validation) MinDimensions ¶
func (v *Validation) MinDimensions(min [2]int) (bool, error)
MinDimensions validates width and height against the [w, h] floor, mirroring `validate_min_dimensions`.
func (*Validation) MinHeight ¶
func (v *Validation) MinHeight(min int) (bool, error)
MinHeight validates that the image is at least min pixels tall, mirroring `validate_min_height`.
func (*Validation) MinSize ¶
func (v *Validation) MinSize(min int64) bool
MinSize validates that the file is at least min bytes, mirroring `validate_min_size`.
Source Files
¶
- add_metadata.go
- attacher.go
- cached_data.go
- data_uri.go
- default_storage.go
- derivatives.go
- determine_mime_type.go
- doc.go
- endpoints.go
- errors.go
- filesystem.go
- memory.go
- metadata.go
- model.go
- pretty_location.go
- refresh_metadata.go
- remote_url.go
- serialization.go
- shrine.go
- signature.go
- storage.go
- store_dimensions.go
- uploaded_file.go
- uploader.go
- validation_helpers.go
