shrine

package module
v0.0.0-...-96ee44b Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 26 Imported by: 0

README

go-ruby-shrine/shrine

shrine — go-ruby-shrine

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's shrine gem — the file-attachment toolkit. It reproduces the Storage abstraction and its built-in Memory / FileSystem backends, the named-storage registry, the Shrine uploader (metadata extraction + location generation), the UploadedFile value with its {"id","storage","metadata"} JSON representation, and the Attacher cache→store promotion lifecycle — without any Ruby runtime.

It is the attachment layer for go-embedded-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 filename/size/mime_type metadata (mime by content sniffing via net/http.DetectContentType), generating a storage location, uploading, JSON-encoding/decoding the UploadedFile, and driving the cache→store promotion 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 (default OSFS; a fake drives the I/O error branches), location generation is the GenerateLocation seam, and MIME detection is the DetectMIME seam. The plugins real apps rely on are implemented here (see Plugins); the parts that need infrastructure the pure-Go core does not carry — storage backends beyond Memory/FileSystem (S3, …), the ORM row, the remote-URL fetch — stay as injectable seams.

Features

Faithful port of the shrine attachment core:

  • StorageUpload(r, id, meta) / Open(id) / Exists(id) / Delete(id) / URL(id, options), with two built-ins: Memory (map-backed, memory:// URLs) and FileSystem (over the injectable FS seam, path or Prefix-based URLs).
  • RegistryNew() plus Register(name, storage) / Storages() (the gem's Shrine.storages[:cache] / [:store]).
  • Uploaders.Uploader(name) then Upload(io, *UploadOptions): extracts metadata (filename from the option, size from the byte length, mime by content sniff), generates a location (default random hex + extension), stores, and returns an UploadedFile. UploadOptions carries Location / Filename / Metadata overrides.
  • UploadedFile{ID, StorageKey, Metadata} with Data() / ToJSON() (the gem's {"id","storage","metadata"} shape), Open / Download / Stream / URL / Exists / Delete, and Replace. s.UploadedFile(json) rehydrates one and binds it to the registry.
  • Attachers.Attacher("cache","store"), then Assign (upload to cache) → Finalize / Promote (promote to store, delete the replaced file) → Destroy, with Changed() state and Set for an already-uploaded file (or nil to detach).
  • SeamsGenerateLocation func(Metadata) string, DetectMIME func([]byte) string, and the filesystem FS interface.
  • Plugins — the core plugin set real apps rely on, in pure Go.

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian) plus js/wasm and wasip1/wasm.

Plugins

The plugins real applications depend on are implemented here in pure Go, on top of the uploader/storage core. Class-level plugins register through the Plugin seam (s.Plugin(&shrine.DetermineMIMEType{})); attachment-level plugins are methods on Attacher / UploadedFile. Every behaviour is checked against the shrine gem (v3.8) and pinned with differential test oracles (signature digests, data: decoding, validation messages, pretty_location shape, …).

plugin this package notes
determine_mime_type DetermineMIMEType + MIMEAnalyzer (ContentAnalyzer, ExtensionAnalyzerFor) content sniff via net/http.DetectContentType
store_dimensions StoreDimensions + DimensionAnalyzer (ImageDimensions); f.Width()/.Height()/.Dimensions() pure-Go image.DecodeConfig (png/jpeg/gif)
add_metadata s.AddMetadata / s.AddMetadataKey; Metadata.String/.Int foundation for the metadata plugins
signature Signature(...) + SignatureMetadata md5/sha1/sha256/sha384/sha512/crc32 × hex/base64/none
refresh_metadata f.RefreshMetadata() recompute metadata from stored bytes
validation_helpers Validation + att.Validate / att.Valid() gem-identical messages; ext case-insensitive, mime case-sensitive
pretty_location s.PrettyLocation(LocationContext{...}) namespace/id/name/basename.ext
derivatives att.CreateDerivatives / AddDerivative / Derivative[URL] / DeleteDerivatives serialised under the column's "derivatives" key
cached_attachment_data att.CachedData() / att.SetCached() hidden-field round-trip
restore_cached_data att.RestoreCachedData() re-extracts metadata from bytes (anti-tamper)
data_uri DataURI(uri) / att.AssignDataURI() RFC 2397 parsing (base64 + form-unescape)
remote_url RemoteURL{Download, MaxSize} + HTTPDownloader download via the Downloader seam
upload_endpoint UploadEndpoint (http.Handler) multipart → cache, JSON response
presign_endpoint PresignEndpoint over the Presigner seam direct-upload descriptor
default_storage DefaultStorage + s.DefaultAttacher() / s.DefaultUploader()
activerecord / sequel ModelAttacher (s.NewActiveRecord / s.NewSequel) over the Record seam callback wiring is host-side
column/entity/model serialization att.ColumnData() / att.LoadColumn() derivatives-aware column data

Left as injectable seams (need infrastructure the pure-Go core does not carry): 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/dimension analyzers are the MIMEAnalyzer / DimensionAnalyzer seams. Beyond that, the interpreter-only plugins (e.g. backgrounding, rack_response, mirroring, infer_extension) remain for the rbgo binding.

MIME divergence. net/http.DetectContentType appends a charset to text (text/plain; charset=utf-8) and never returns nil, where the gem's default :file analyzer returns a bare type or nil for empty/unknown content. The recognised binary signatures (PNG/JPEG/GIF/PDF/ZIP/…) agree with the gem.

Install

go get github.com/go-ruby-shrine/shrine

Usage

package main

import (
	"fmt"
	"strings"

	"github.com/go-ruby-shrine/shrine"
)

func main() {
	s := shrine.New()
	s.Register("cache", shrine.NewMemory())
	s.Register("store", shrine.NewFileSystem("/var/uploads"))

	// Direct upload.
	up, _ := s.Uploader("store")
	file, _ := up.Upload(strings.NewReader("hello"), &shrine.UploadOptions{Filename: "hello.txt"})
	data, _ := file.ToJSON()
	fmt.Println(data) // {"id":"….txt","storage":"store","metadata":{…}}

	// Rehydrate the reference and read it back.
	file2, _ := s.UploadedFile(data)
	body, _ := file2.Download()
	fmt.Println(string(body)) // hello

	// Attach lifecycle: assign to cache, finalize promotes to store.
	att, _ := s.Attacher("cache", "store")
	att.Assign(strings.NewReader("world"), &shrine.UploadOptions{Filename: "w.txt"})
	att.Finalize() // promoted to store; a replaced file is deleted
	fmt.Println(att.Get().StorageKey) // store
}
Injecting the seams (tests / hosts)
s := shrine.New()
s.GenerateLocation = func(m shrine.Metadata) string { return "fixed-id-" + m.Filename() }
s.DetectMIME = func([]byte) string { return "application/octet-stream" }

// Filesystem storage over a fake FS keeps tests off the disk.
s.Register("store", shrine.NewFileSystemWithFS("/base", myFakeFS{}))

Value model

gem this package
Shrine.storages[:cache] = … s.Register("cache", …) / s.Storages()
Shrine.new(:store).upload(io) s.Uploader("store") then up.Upload(io, opts)
Shrine::UploadedFile#data / #to_json (*UploadedFile).Data() / .ToJSON()
uploaded_file.url / #download / #metadata .URL(opts) / .Download() / .Metadata
Shrine.uploaded_file(data) s.UploadedFile(json)
Attacher#assign / #finalize / #destroy att.Assign / .Finalize / .Destroy
Attacher#changed? att.Changed()
Shrine::Storage::Memory / ::FileSystem NewMemory() / NewFileSystem(dir)
the storage I/O FS seam (OSFS prod, fake in tests)
JSON data shape
{"id":"49f7c1….txt","storage":"store","metadata":{"filename":"hello.txt","mime_type":"text/plain; charset=utf-8","size":5}}

Tests & coverage

The suite is deterministic and touches no real network. Filesystem I/O runs over the FS seam — a fake drives every error branch (mkdir/write/read/remove failure) while OSFS is exercised against t.TempDir(); a DoerFunc-style errReader/errWriter cover the streaming error paths; the entropy source and the location/MIME seams are injected. No test opens a real socket, so the cross-arch qemu lanes and the Windows lane all hold coverage at 100%.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-shrine/shrine authors.

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.

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

Constants

This section is empty.

Variables

View Source
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`.

View Source
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"`.

View Source
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.

View Source
var ErrNotFound = errors.New("shrine: file not found")

ErrNotFound is returned (wrapped) when opening an id that does not exist in a storage.

View Source
var ErrRemoteURLDownload = fmt.Errorf("shrine: remote url download failed")

ErrRemoteURLDownload wraps a Downloader failure.

View Source
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.

View Source
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.

View Source
var ErrUnknownFormat = fmt.Errorf("shrine: unknown signature format")

ErrUnknownFormat is returned (wrapped) by Signature for an unsupported format.

View Source
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

func ContentAnalyzer(data []byte) string

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

func DataURI(uri string) (mimeType string, data []byte, err error)

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

func ImageDimensions(data []byte) (int, int, bool)

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

func (a *Attacher) AddDerivative(name string, r io.Reader, opts *UploadOptions) error

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

func (a *Attacher) CachedData() (string, error)

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

func (a *Attacher) Changed() bool

Changed reports whether the attachment differs from the last finalized state, mirroring `Attacher#changed?`.

func (*Attacher) ColumnData

func (a *Attacher) ColumnData() (string, error)

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

func (a *Attacher) CreateDerivatives(deriver Deriver) error

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

func (a *Attacher) DeleteDerivatives() error

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

func (a *Attacher) DerivativeURL(name string, opts map[string]any) string

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) Destroy

func (a *Attacher) Destroy() error

Destroy deletes the current attachment, mirroring `Attacher#destroy`.

func (*Attacher) Finalize

func (a *Attacher) Finalize() error

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

func (a *Attacher) LoadColumn(data string) error

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

func (a *Attacher) Promote() error

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

func (a *Attacher) RestoreCachedData(json string) error

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

func (a *Attacher) SetCached(json string) error

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.

func (*Attacher) Valid

func (a *Attacher) Valid() bool

Valid reports whether the last attachment change passed validation (no recorded errors), mirroring `Attacher#valid?`.

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

type Deriver func(original []byte) (map[string]io.Reader, error)

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

type DimensionAnalyzer func(data []byte) (width, height int, ok bool)

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.

func (*FileSystem) URL

func (s *FileSystem) URL(id string, _ map[string]any) string

URL returns Prefix + "/" + id when Prefix is set, else the on-disk path.

func (*FileSystem) Upload

func (s *FileSystem) Upload(r io.Reader, id string, _ map[string]any) error

Upload writes the bytes read from r to the file for id, creating parents.

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

type MIMEAnalyzer func(data []byte) string

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 NewMemory

func NewMemory() *Memory

NewMemory returns an empty in-memory storage.

func (*Memory) Delete

func (m *Memory) Delete(id string) error

Delete removes id (a no-op if absent).

func (*Memory) Exists

func (m *Memory) Exists(id string) bool

Exists reports whether id is present.

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.

func (*Memory) URL

func (m *Memory) URL(id string, _ map[string]any) string

URL returns "memory://<id>".

func (*Memory) Upload

func (m *Memory) Upload(r io.Reader, id string, _ map[string]any) error

Upload buffers the bytes read from r under id.

type Metadata

type Metadata map[string]any

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) Filename

func (m Metadata) Filename() string

Filename returns the "filename" value, or "" if absent/null.

func (Metadata) Int

func (m Metadata) Int(key string) 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) MimeType

func (m Metadata) MimeType() string

MimeType returns the "mime_type" value, or "" if absent/null.

func (Metadata) Size

func (m Metadata) Size() int64

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

func (m Metadata) String(key string) 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

type MetadataExtractor func(data []byte, meta Metadata) map[string]any

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.

func (OSFS) Exists

func (OSFS) Exists(path string) bool

Exists reports whether path exists via os.Stat.

func (OSFS) MkdirAll

func (OSFS) MkdirAll(dir string, perm os.FileMode) error

MkdirAll calls os.MkdirAll.

func (OSFS) ReadFile

func (OSFS) ReadFile(path string) ([]byte, error)

ReadFile calls os.ReadFile.

func (OSFS) Remove

func (OSFS) Remove(path string) error

Remove calls os.Remove.

func (OSFS) WriteFile

func (OSFS) WriteFile(path string, data []byte, perm os.FileMode) error

WriteFile calls os.WriteFile.

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 New

func New() *Shrine

New returns a Shrine with an empty registry and the default seams.

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

func (s *Shrine) AddMetadataKey(name string, fn func(data []byte, meta Metadata) any)

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

func (s *Shrine) Attacher(cacheKey, storeKey string) (*Attacher, error)

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

func (s *Shrine) DefaultAttacher() (*Attacher, error)

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

func (s *Shrine) DefaultUploader() (*Uploader, error)

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) Plugin

func (s *Shrine) Plugin(p Plugin)

Plugin registers p and invokes its Configure hook.

func (*Shrine) Plugins

func (s *Shrine) Plugins() []Plugin

Plugins returns the registered plugins in registration order.

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

func (s *Shrine) Register(name string, st Storage)

Register adds st to the registry under name (e.g. "cache" or "store"), mirroring assigning to `Shrine.storages[:name]`.

func (*Shrine) Storages

func (s *Shrine) Storages() map[string]Storage

Storages returns the storage registry, mirroring `Shrine.storages`.

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)`.

func (*Shrine) Uploader

func (s *Shrine) Uploader(name string) (*Uploader, error)

Uploader returns an Uploader bound to the storage registered under name, or a wrapped ErrUnknownStorage.

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

func (u *Uploader) StorageKey() string

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`.

func (*Validation) MinWidth

func (v *Validation) MinWidth(min int) (bool, error)

MinWidth validates that the image is at least min pixels wide, mirroring `validate_min_width`.

Jump to

Keyboard shortcuts

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