activestorage

package module
v0.0.0-...-16cc7b4 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: 21 Imported by: 0

README

go-ruby-activestorage/activestorage

activestorage — go-ruby-activestorage

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Rails' Active Storage — the framework for attaching files to records and storing them on a pluggable backend. It reproduces Active Storage's observable behaviour byte-for-byte — sharded storage keys, base64 MD5 checksums, has_one_attached / has_many_attached semantics, message-verifier signed_ids, Marcel-style content-type detection, variation digests / variant keys, and the direct-upload token shape — without any Ruby runtime. The on-disk key layout, checksum encoding, signed-id / variation-key format, and Marcel detection are pinned by a live differential test against the activestorage gem (skipped when the gem is absent).

It is the Active Storage backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-set and the other go-ruby-* stdlib ports.

Persistence is a seam. Active Storage's Blob and Attachment are ActiveRecord models. This library keeps the model logic and abstracts the database behind the ModelStore interface, so a host binds it to ActiveRecord (or anything else). A goroutine-safe in-memory MemStore ships as the reference implementation.

Seams

Everything host-specific is injected through a small set of interfaces, gathered in a Config:

Seam Interface Rails analogue Default provided
Persistence ModelStore ActiveRecord (active_storage_blobs, active_storage_attachments) MemStore (in-memory)
Storage Service ActiveStorage::Service DiskService (local, sharded)
Signing Signer ActiveSupport::MessageVerifier (signed_id) HMACSigner (HMAC-SHA256)
Randomness RandomSource SecureRandom.base36 (storage keys) crypto/rand-backed
Clock func() time.Time Time.current time.Now().UTC

Install

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

Usage

package main

import (
	"fmt"
	"strings"

	as "github.com/go-ruby-activestorage/activestorage"
)

func main() {
	cfg := &as.Config{
		Store:    as.NewMemStore(),
		Services: as.NewRegistry().Register(as.NewDiskService("local", "/var/storage")),
		Signer:   as.NewHMACSigner([]byte("secret-key-base")),
	}

	// Create a blob and stream its bytes to the service (create_and_upload!).
	blob, _ := cfg.CreateAndUpload(strings.NewReader("hello world"), as.BlobParams{
		Filename: "greeting.txt",
	})
	fmt.Println(blob.Key)        // 28-char base36 storage key
	fmt.Println(blob.Checksum)   // XrY7u+Ae7tCTyyK7j1rNww==  (Digest::MD5.base64digest)
	fmt.Println(blob.ContentType) // inferred from the filename

	// Attach it to a record: has_one_attached :avatar on User#1.
	user := as.RecordRef{Type: "User", ID: 1}
	cfg.One(user, "avatar").Attach(blob)

	// has_many_attached :photos — attach an existing blob and a fresh upload.
	cfg.Many(user, "photos").Attach(
		blob,
		as.Upload{Filename: "sunset.txt", Reader: strings.NewReader("...")},
	)

	// A tamper-evident id to embed in a URL, then resolve it back.
	sid, _ := blob.SignedID()
	again, _ := cfg.FindSignedBlob(sid)
	fmt.Println(again.ID == blob.ID) // true

	data, _ := blob.Download()
	fmt.Println(string(data)) // hello world
}

What ships

  • Service abstraction — a key-addressed, streaming interface (Upload / Download / DownloadChunk / Delete / Exist / Url / Size), shaped so S3/GCS/Azure services can implement the same contract, plus a Registry of configured services with a default.
  • DiskService — stores each object at root/<key[0:2]>/<key[2:4]>/<key> (Rails' two-level sharding), verifies the base64 MD5 checksum on upload, and supports ranged reads. All temp/handle cleanup closes files before removing them, so it is safe on Windows.
  • Blobcreate_and_upload!, build_after_upload, create_before_direct_upload!, upload, download, download_chunk, open (checksum-verified, Windows-safe temp file), purge, signed_id / signed-id lookup, content-type inference, and 28-char base36 key generation with an injectable RNG.
  • Attachmenthas_one_attached (OneAttached) and has_many_attached (ManyAttached) semantics: Attach (of a *Blob, an Upload{io,…}, or a signed id), Attached, Blob(s), Detach, Purge, and the join record.
  • RailsVerifier — a Signer/Verifier that reproduces ActiveSupport::MessageVerifier byte-for-byte (JSON serializer, SHA-1 HMAC, url-safe payload, the {"_rails":{"message","exp","pur"}} metadata envelope), so a blob's signed_id, a variation key, and the disk service's direct-upload token are identical to a Rails app configured with the same secret. HMACSigner remains as a self-contained default.
  • Content-type detectionDetectContentType(data, name), a faithful subset of Marcel::MimeType.for: leading magic bytes first (PNG/JPEG/GIF/PDF/ZIP/…), the xml→svg / zip→docx "more specific" upgrades, then extension fallback, wired into the upload path (identify: honoured).
  • Variants / representationsVariation (transformations descriptor, Digest = SHA1.base64digest(Marshal.dump(transformations)) via an internal MRI-exact Ruby-Marshal encoder, Key = the signed variation key), Variant (combined variants/<key>/<sha256> service key, filename, content type, Processed/Process), and VariantRecord. Actual pixel work is delegated to an injectable Transformer seam (e.g. go-images) — no libvips/CGO required.
  • Direct uploadsCreateForDirectUpload / Blob.DirectUpload return the {signed_id, url, headers} shape of DirectUploadsController#create; the DiskService mints the signed :blob_token URL (DirectUploadService).
  • Filename — base / extension (Ruby File.extname semantics, incl. dotfiles) and sanitized matching the gem's exact tr character set.

Roadmap (deferred)

The following Active Storage surface is intentionally not implemented yet and is tracked for later releases:

  • Actual image transformation — the Variant/Variation descriptors (key, digest, filename, content type) ship and match the gem; producing the transformed bytes needs an injected Transformer (this repo ships none, to stay CGO-free — a go-images-backed transformer is the intended companion).
  • Analyzers (image/video/audio metadata) and previewers (PDF/video poster frames).
  • Cloud services — S3, Google Cloud Storage, Azure, and the mirror service — layered on the existing Service interface.
  • The Rails engine — routes, controllers, the disk/blob/representation redirect and proxy controllers, and signed download URLs backed by the Rails routing layer (the direct-upload token itself ships).
  • Streaming uploads — an upload is buffered in memory to compute its checksum and size; a streaming/rewindable path is future work.

Tests & coverage

The suite is deterministic and Ruby-free. Hard-to-provoke syscall error paths (a failing Create/Close, an injected Stat/Remove, temp-file failures) are reached through small filesystem seams, and the persistence/service/signer seams are exercised with in-memory fakes and a temp-dir DiskService, holding line coverage at 100% including every error branch. The base64 MD5 checksum is asserted against Rails' Digest::MD5.base64digest format.

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

CGO-free, dependency-free, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — including the big-endian s390x) and three OSes (Linux, macOS, Windows).

License

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

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, …)

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrIntegrity is returned when an uploaded object's checksum does not match
	// the expected value (Rails' ActiveStorage::IntegrityError).
	ErrIntegrity = errors.New("activestorage: integrity check failed")
	// ErrServiceNotFound is returned by a Registry when a named service (or the
	// default) is not configured.
	ErrServiceNotFound = errors.New("activestorage: service not registered")
	// ErrNotDirectUploadable is returned when a direct upload is requested from a
	// service that does not implement DirectUploadService.
	ErrNotDirectUploadable = errors.New("activestorage: service does not support direct uploads")
)

Service-layer sentinel errors.

View Source
var (
	ErrBlobNotFound       = errors.New("activestorage: blob not found")
	ErrAttachmentNotFound = errors.New("activestorage: attachment not found")
)

ErrBlobNotFound and ErrAttachmentNotFound are the sentinel errors a ModelStore returns when a lookup or mutation targets a row that does not exist.

View Source
var ErrExpired = errors.New("activestorage: signed message has expired")

ErrExpired is returned by a Verifier when a token carried an expiry that has already passed (ActiveSupport::MessageVerifier::InvalidSignature on expiry).

View Source
var ErrInvalidSignature = errors.New("activestorage: invalid signature")

ErrInvalidSignature is returned by a Signer.Verify when a token is malformed, tampered with, or signed for a different purpose.

View Source
var ErrNotTransformable = errors.New("activestorage: no image transformer configured")

ErrNotTransformable is returned when a variant's bytes are requested on a Config whose Transformer seam is unset. Producing variant bytes needs a real image processor (go-images, libvips, ImageMagick); the descriptor records — a variant's key, digest, filename, and content type — never do.

View Source
var ErrUnattachable = errors.New("activestorage: value is not attachable")

ErrUnattachable is returned when a value passed to Attach is not something the library knows how to turn into a Blob.

Functions

func ContentTypeForFilename

func ContentTypeForFilename(name string) string

ContentTypeForFilename infers a MIME type from a filename's extension, dropping any parameters (e.g. "; charset=utf-8") to return a bare "type/subtype". When the extension is missing or unknown it returns application/octet-stream, the same conservative default Rails applies when content-type detection fails.

func DetectContentType

func DetectContentType(data []byte, name string) string

DetectContentType reproduces ActiveStorage's extract_content_type, which is Marcel::MimeType.for(io, name:) for the common cases: it prefers the type implied by the leading magic bytes, upgrades a generic magic type to a more specific one implied by the name (xml→svg, zip→docx, …), and, when no magic matches, falls back to the name's extension — returning application/octet-stream only when neither yields anything.

Types

type Attachment

type Attachment struct {
	ID         int64
	RecordType string
	RecordID   int64
	Name       string
	BlobID     int64
	CreatedAt  time.Time
	// contains filtered or unexported fields
}

Attachment is the join record binding a blob to a named association on a record — ActiveStorage::Attachment (record, name, blob).

func (*Attachment) Blob

func (a *Attachment) Blob() (*Blob, error)

Blob loads the attached blob, caching it on the attachment.

func (*Attachment) Purge

func (a *Attachment) Purge() error

Purge deletes the attachment record and purges its blob — ActiveStorage::Attachment#purge.

type Blob

type Blob struct {
	ID          int64
	Key         string
	Filename    string
	ContentType string
	Metadata    map[string]any
	ByteSize    int64
	Checksum    string
	ServiceName string
	CreatedAt   time.Time
	// contains filtered or unexported fields
}

Blob is the persisted description of an uploaded file: where it lives (Key, ServiceName), what it is (Filename, ContentType, Metadata), and how to verify it (ByteSize, Checksum). It mirrors ActiveStorage::Blob's stored columns. The underlying database row is managed through the Config's ModelStore seam.

func (*Blob) DirectUpload

func (b *Blob) DirectUpload(expiresIn time.Duration) (DirectUpload, error)

DirectUpload returns the signed URL, headers, and signed id a client needs to upload the blob's bytes directly to its service — ActiveStorage's service_url_for_direct_upload / service_headers_for_direct_upload / signed_id. It requires the blob's service to implement DirectUploadService.

func (*Blob) Download

func (b *Blob) Download() ([]byte, error)

Download returns the blob's full contents from its service.

func (*Blob) DownloadChunk

func (b *Blob) DownloadChunk(offset, length int64) ([]byte, error)

DownloadChunk returns length bytes of the blob starting at offset.

func (*Blob) Open

func (b *Blob) Open(fn func(*os.File) error) error

Open downloads the blob to a temporary file, verifies its checksum, and yields the open file positioned at the start. The temp file is always closed before it is removed, so the cleanup is safe on Windows (where an open file cannot be deleted). Mirrors ActiveStorage::Blob#open.

func (*Blob) Purge

func (b *Blob) Purge() error

Purge deletes the blob's stored object and its database row — ActiveStorage::Blob#purge. Variant/attachment cascade purging is deferred (see the roadmap).

func (*Blob) Service

func (b *Blob) Service() (Service, error)

Service resolves the blob's service through the registry.

func (*Blob) SignedID

func (b *Blob) SignedID() (string, error)

SignedID returns a tamper-evident, purpose-scoped id for the blob, suitable for embedding in a URL — ActiveStorage::Blob#signed_id. With a RailsVerifier the token is byte-identical to the gem's (the integer id, purpose "blob_id", message-verifier envelope); with a plain Signer the id is signed as a string.

func (*Blob) URL

func (b *Blob) URL(opts URLOptions) (string, error)

URL returns a service URL for the blob, defaulting the filename, content type, and disposition from the blob when the options leave them unset.

func (*Blob) Upload

func (b *Blob) Upload(r io.Reader) error

Upload streams r to the blob's service, updating ByteSize and Checksum from the bytes read — ActiveStorage::Blob#upload.

func (*Blob) Variant

func (b *Blob) Variant(transformations Hash) *Variant

Variant returns the variant of b described by transformations. The blob's Config supplies the verifier (for the key) and the Transformer (for Process).

type BlobParams

type BlobParams struct {
	Filename    string
	ContentType string
	Metadata    map[string]any
	ServiceName string
	Key         string
	ByteSize    int64
	Checksum    string
	// Identify forces content-type detection from the uploaded bytes even when
	// ContentType is set, mirroring Active Storage's identify: true default. When
	// false, a caller-supplied ContentType is trusted (identify: false).
	Identify bool
}

BlobParams describes a blob to build. Key and ContentType are derived when empty (a fresh base36 key; a MIME type inferred from Filename). ByteSize and Checksum are set automatically by the upload helpers, or supplied directly for a direct-upload blob.

type Config

type Config struct {
	Store       ModelStore
	Services    *Registry
	Signer      Signer
	Random      RandomSource     // defaults to DefaultRandom() when nil
	Clock       func() time.Time // defaults to time.Now().UTC when nil
	Transformer Transformer      // image-processing seam for variants; nil = variants unprocessable
}

Config bundles the pluggable seams the model logic depends on: the persistence store (ActiveRecord), the service registry, the signer (signed ids), the randomness source (storage keys), and a clock. It is the entry point for creating and finding blobs and attachments.

func (*Config) BuildAfterUpload

func (c *Config) BuildAfterUpload(r io.Reader, p BlobParams) (*Blob, error)

BuildAfterUpload builds a blob from r and params and uploads it, but does not persist the row — ActiveStorage::Blob.build_after_upload.

func (*Config) CreateAndUpload

func (c *Config) CreateAndUpload(r io.Reader, p BlobParams) (*Blob, error)

CreateAndUpload builds a blob from r and params, persists the row, then uploads the bytes to the blob's service — ActiveStorage::Blob.create_and_upload!.

func (*Config) CreateBeforeDirectUpload

func (c *Config) CreateBeforeDirectUpload(p BlobParams) (*Blob, error)

CreateBeforeDirectUpload builds and persists a blob from params (whose ByteSize and Checksum are already known) without uploading any bytes — the client will PUT them directly. Mirrors ActiveStorage::Blob.create_before_direct_upload!.

func (*Config) CreateForDirectUpload

func (c *Config) CreateForDirectUpload(p BlobParams, expiresIn time.Duration) (*Blob, DirectUpload, error)

CreateForDirectUpload persists a blob from params (whose ByteSize and Checksum the client precomputed) and returns it alongside the DirectUpload payload the client uses to PUT the bytes and later attach the blob — the response shape of ActiveStorage::DirectUploadsController#create. expiresIn bounds the signed URL.

func (*Config) FindBlob

func (c *Config) FindBlob(id int64) (*Blob, error)

FindBlob loads a blob row by id.

func (*Config) FindSignedBlob

func (c *Config) FindSignedBlob(signedID string) (*Blob, error)

FindSignedBlob verifies a signed id and loads the blob it names. When the configured Signer is a Verifier (RailsVerifier), the id is decoded from the message-verifier payload (a JSON integer); otherwise the string Signer path is used.

func (*Config) Many

func (c *Config) Many(record RecordRef, name string) *ManyAttached

Many returns the has_many_attached proxy for name on record.

func (*Config) One

func (c *Config) One(record RecordRef, name string) *OneAttached

One returns the has_one_attached proxy for name on record.

type DirectUpload

type DirectUpload struct {
	SignedID string
	URL      string
	Headers  map[string]string
}

DirectUpload is the payload a client needs to upload straight to the service and then attach the blob — the shape ActiveStorage::DirectUploadsController returns: the blob's SignedID plus the upload URL and the required headers.

type DirectUploadOptions

type DirectUploadOptions struct {
	ContentType   string
	ContentLength int64
	Checksum      string
	ExpiresIn     time.Duration
}

DirectUploadOptions carries the blob facts a service embeds in a signed direct-upload token — the content type, length, and checksum the client must match, and how long the URL stays valid.

type DirectUploadService

type DirectUploadService interface {
	URLForDirectUpload(key string, opts DirectUploadOptions) (string, error)
	HeadersForDirectUpload(key, contentType string) map[string]string
}

DirectUploadService is the optional capability of a Service that can mint a signed direct-upload URL and the headers a client must send with its PUT, mirroring ActiveStorage::Service#url_for_direct_upload and #headers_for_direct_upload.

type DiskService

type DiskService struct {

	// Verifier signs direct-upload tokens; nil disables DirectUploadService.
	Verifier Verifier
	// URLPrefix is prepended to a direct-upload token; defaults to
	// "/rails/active_storage/disk/" when empty.
	URLPrefix string
	// Clock supplies "now" for token expiry; defaults to time.Now.
	Clock func() time.Time
	// contains filtered or unexported fields
}

DiskService is the built-in local-filesystem Service. It stores each object at Root/<key[0:2]>/<key[2:4]>/<key>, the same two-level sharding Rails' ActiveStorage::Service::DiskService uses to keep directories shallow.

When Verifier and URLPrefix are set it also mints signed direct-upload tokens (DirectUploadService) the same way ActiveStorage::Service::DiskService does.

func NewDiskService

func NewDiskService(name, root string) *DiskService

NewDiskService returns a DiskService rooted at root and registered as name.

func (*DiskService) Delete

func (d *DiskService) Delete(key string) error

Delete removes the object under key. A missing key is not an error (idempotent, like Rails rescuing Errno::ENOENT).

func (*DiskService) Download

func (d *DiskService) Download(key string) (io.ReadCloser, error)

Download opens the object stored under key for reading. The caller closes it.

func (*DiskService) DownloadChunk

func (d *DiskService) DownloadChunk(key string, offset, length int64) ([]byte, error)

DownloadChunk returns length bytes of the object under key starting at offset.

func (*DiskService) Exist

func (d *DiskService) Exist(key string) (bool, error)

Exist reports whether an object is stored under key.

func (*DiskService) HeadersForDirectUpload

func (d *DiskService) HeadersForDirectUpload(_ string, contentType string) map[string]string

HeadersForDirectUpload returns the headers a client must send with a direct upload — just the Content-Type, as ActiveStorage::Service::DiskService does.

func (*DiskService) Name

func (d *DiskService) Name() string

Name returns the service's registry name.

func (*DiskService) Root

func (d *DiskService) Root() string

Root returns the directory under which objects are stored.

func (*DiskService) Size

func (d *DiskService) Size(key string) (int64, error)

Size returns the stored byte size of the object under key.

func (*DiskService) URLForDirectUpload

func (d *DiskService) URLForDirectUpload(key string, o DirectUploadOptions) (string, error)

URLForDirectUpload returns a signed URL a client can PUT bytes to, embedding a message-verifier token over {key, content_type, content_length, checksum, service_name} scoped to purpose :blob_token — matching ActiveStorage::Service::DiskService#url_for_direct_upload.

func (*DiskService) Upload

func (d *DiskService) Upload(key string, r io.Reader, checksum string) error

Upload writes r under key, creating the sharded directory as needed and, when checksum is non-empty, verifying the stored bytes' base64 MD5 against it (deleting the file and returning ErrIntegrity on mismatch).

func (*DiskService) Url

func (d *DiskService) Url(key string, opts URLOptions) (string, error)

Url returns a disk-service URL for key. Without the Rails routing/engine layer (deferred, see the roadmap) this is a representative, disposition-annotated path rather than a signed, routable URL.

type Entry

type Entry struct {
	Key   string
	Value any
}

Entry is one key/value pair of an ordered Ruby hash. The key is a Symbol name (Active Storage deep-symbolizes transformation keys and builds its message-verifier payloads with symbol keys). Value is one of: int, int64, string, Symbol, bool, nil, []any, or Hash.

type Filename

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

Filename is a value type mirroring ActiveStorage::Filename: it exposes a file's base name, extension, and a sanitized form safe to persist or serve.

func NewFilename

func NewFilename(name string) Filename

NewFilename wraps a raw filename string.

func (Filename) Base

func (f Filename) Base() string

Base returns the basename with its extension removed (ActiveStorage::Filename#base).

func (Filename) Extension

func (f Filename) Extension() string

Extension returns the extension without its leading dot (e.g. "pdf"), or "".

func (Filename) ExtensionWithDelimiter

func (f Filename) ExtensionWithDelimiter() string

ExtensionWithDelimiter returns the extension including its leading dot (e.g. ".pdf"), or "" when there is none.

func (Filename) Sanitized

func (f Filename) Sanitized() string

Sanitized returns the filename with surrounding whitespace trimmed and unsafe characters replaced by "-".

func (Filename) String

func (f Filename) String() string

String returns the sanitized filename, matching ActiveStorage::Filename#to_s.

type HMACSigner

type HMACSigner struct {
	Secret []byte
}

HMACSigner is the default Signer: it authenticates a payload with HMAC-SHA256 and encodes the token as "purpose.base64url(data).base64url(mac)". It is not a drop-in for Rails' exact wire format (that is a MessageVerifier concern), but it is a complete, self-contained default for standalone use.

func NewHMACSigner

func NewHMACSigner(secret []byte) *HMACSigner

NewHMACSigner returns an HMACSigner keyed by secret.

func (*HMACSigner) Sign

func (s *HMACSigner) Sign(data, purpose string) (string, error)

Sign returns a signed token binding data to purpose.

func (*HMACSigner) Verify

func (s *HMACSigner) Verify(token, purpose string) (string, error)

Verify checks a token's signature and purpose, returning the original data.

type Hash

type Hash []Entry

Hash is an ordered Ruby hash with symbol keys. Order is significant: it is the insertion order Ruby preserves, which fixes the byte layout of both JSON.dump and Marshal.dump (and hence a signed key or a variation digest). It is used for variation transformations and for message-verifier payloads.

func (Hash) Get

func (h Hash) Get(key string) (any, bool)

Get returns the value stored under key and whether it was present.

type ManyAttached

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

ManyAttached models a has_many_attached association: an ordered set of named attachments on a record.

func (*ManyAttached) Attach

func (m *ManyAttached) Attach(attachables ...any) ([]*Attachment, error)

Attach appends each attachable to the record, returning the new attachments. On the first failure it returns the attachments created so far and the error.

func (*ManyAttached) Attached

func (m *ManyAttached) Attached() (bool, error)

Attached reports whether at least one attachment is present.

func (*ManyAttached) Attachments

func (m *ManyAttached) Attachments() ([]*Attachment, error)

Attachments returns the current attachments in insertion order.

func (*ManyAttached) Blobs

func (m *ManyAttached) Blobs() ([]*Blob, error)

Blobs returns the attached blobs in order.

func (*ManyAttached) Detach

func (m *ManyAttached) Detach() error

Detach removes all attachment records, leaving their blobs in place.

func (*ManyAttached) Purge

func (m *ManyAttached) Purge() error

Purge removes all attachment records and purges their blobs.

type MemStore

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

MemStore is a goroutine-safe, in-memory ModelStore. It is the reference implementation used in tests and suitable for the rbgo binding's scratch use; it is not durable.

func NewMemStore

func NewMemStore() *MemStore

NewMemStore returns an empty in-memory ModelStore.

func (*MemStore) DeleteAttachment

func (m *MemStore) DeleteAttachment(id int64) error

DeleteAttachment removes the attachment with the given id, or returns ErrAttachmentNotFound.

func (*MemStore) DeleteBlob

func (m *MemStore) DeleteBlob(id int64) error

DeleteBlob removes the blob with the given id, or returns ErrBlobNotFound.

func (*MemStore) FindAttachments

func (m *MemStore) FindAttachments(recordType string, recordID int64, name string) ([]*Attachment, error)

FindAttachments returns the attachments for a record and name, in insertion order.

func (*MemStore) FindBlob

func (m *MemStore) FindBlob(id int64) (*Blob, error)

FindBlob returns the blob with the given id, or ErrBlobNotFound.

func (*MemStore) InsertAttachment

func (m *MemStore) InsertAttachment(a *Attachment) error

InsertAttachment assigns a.ID and stores the attachment.

func (*MemStore) InsertBlob

func (m *MemStore) InsertBlob(b *Blob) error

InsertBlob assigns b.ID and stores the blob.

func (*MemStore) UpdateBlob

func (m *MemStore) UpdateBlob(b *Blob) error

UpdateBlob replaces the stored blob with the given id, or returns ErrBlobNotFound.

type ModelStore

type ModelStore interface {
	InsertBlob(b *Blob) error
	FindBlob(id int64) (*Blob, error)
	UpdateBlob(b *Blob) error
	DeleteBlob(id int64) error

	InsertAttachment(a *Attachment) error
	FindAttachments(recordType string, recordID int64, name string) ([]*Attachment, error)
	DeleteAttachment(id int64) error
}

ModelStore is the persistence seam. In Rails, Blob and Attachment are ActiveRecord models backed by the active_storage_blobs and active_storage_attachments tables; this interface abstracts that away so the model logic here has no database dependency. A host binds it to ActiveRecord (or any datastore); NewMemStore provides an in-memory reference implementation.

Insert* assigns the row's ID field. Find/Update/Delete operate by ID.

type OneAttached

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

OneAttached models a has_one_attached association: a single, named attachment on a record.

func (*OneAttached) Attach

func (o *OneAttached) Attach(attachable any) (*Attachment, error)

Attach binds attachable to the record, replacing any existing attachment (the previous join record is removed). It returns the new attachment.

func (*OneAttached) Attached

func (o *OneAttached) Attached() (bool, error)

Attached reports whether an attachment is present.

func (*OneAttached) Attachment

func (o *OneAttached) Attachment() (*Attachment, error)

Attachment returns the current attachment, or nil when none is attached. If several rows exist (from prior attaches), the most recent wins.

func (*OneAttached) Blob

func (o *OneAttached) Blob() (*Blob, error)

Blob returns the attached blob, or nil when none is attached.

func (*OneAttached) Detach

func (o *OneAttached) Detach() error

Detach removes the attachment record, leaving its blob in place — ActiveStorage's detach.

func (*OneAttached) Purge

func (o *OneAttached) Purge() error

Purge removes the attachment record and purges its blob.

type RailsVerifier

type RailsVerifier struct {
	Secret  []byte
	URLSafe bool             // url-safe Base64 payload (Rails' message_verifier default)
	Now     func() time.Time // clock for expiry checks; defaults to time.Now
}

RailsVerifier reproduces ActiveSupport::MessageVerifier byte-for-byte for the serializer Rails uses by default (JSON) with a SHA-1 HMAC. Tokens have the wire form "<payload>--<hex-hmac>", where <payload> is url-safe (or strict) Base64 of either the bare serialized value or, when a purpose or expiry is present, the {"_rails":{"message":…,"exp":…,"pur":…}} metadata envelope. It implements both Signer (for string payloads) and Verifier (for typed payloads), so it can be a drop-in Config.Signer that makes blob signed_ids and variation keys match a running Rails app configured with the same secret.

func NewRailsVerifier

func NewRailsVerifier(secret []byte) *RailsVerifier

NewRailsVerifier returns a RailsVerifier keyed by secret, using the url-safe Base64 encoding Rails' application message verifier uses.

func (*RailsVerifier) Generate

func (v *RailsVerifier) Generate(value any, purpose string, expiresAt time.Time) (string, error)

Generate implements Verifier.

func (*RailsVerifier) Sign

func (v *RailsVerifier) Sign(data, purpose string) (string, error)

Sign implements Signer by treating data as a string value (Rails serializes it as a JSON string). For typed payloads use Generate.

func (*RailsVerifier) Verified

func (v *RailsVerifier) Verified(token, purpose string) ([]byte, error)

Verified implements Verifier.

func (*RailsVerifier) Verify

func (v *RailsVerifier) Verify(token, purpose string) (string, error)

Verify implements Signer, returning the original string value.

type RandomSource

type RandomSource interface {
	// RandomBytes returns n cryptographically random bytes.
	RandomBytes(n int) ([]byte, error)
	// RandomNumber returns a uniform integer in [0, max).
	RandomNumber(max int) (int, error)
}

RandomSource is the randomness seam. The default implementation is backed by crypto/rand; tests (and deterministic hosts) can inject their own. It mirrors the two primitives Ruby's SecureRandom.base36 relies on: random_bytes and random_number.

func DefaultRandom

func DefaultRandom() RandomSource

DefaultRandom returns the crypto/rand-backed RandomSource used when a Config leaves Random unset.

type RecordRef

type RecordRef struct {
	Type string
	ID   int64
}

RecordRef identifies the owning record of an attachment by its polymorphic type and id — the record_type / record_id columns of active_storage_attachments.

type Registry

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

Registry holds the configured services and the default one (Rails config.active_storage.service). It is the resolver a Blob uses to turn its service_name into a Service.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry.

func (*Registry) Default

func (r *Registry) Default() (Service, error)

Default returns the default service, or ErrServiceNotFound if none is set.

func (*Registry) Fetch

func (r *Registry) Fetch(name string) (Service, error)

Fetch returns the service registered under name, or ErrServiceNotFound.

func (*Registry) Register

func (r *Registry) Register(s Service) *Registry

Register adds s under its Name(). If no default is set yet, the first registered service becomes the default. It returns the Registry for chaining.

func (*Registry) SetDefault

func (r *Registry) SetDefault(name string) *Registry

SetDefault marks name as the default service.

type Service

type Service interface {
	// Name returns the service's registry name.
	Name() string
	// Upload stores r under key, verifying the base64 MD5 checksum when non-empty.
	Upload(key string, r io.Reader, checksum string) error
	// Download opens the object stored under key for reading.
	Download(key string) (io.ReadCloser, error)
	// DownloadChunk returns length bytes starting at offset.
	DownloadChunk(key string, offset, length int64) ([]byte, error)
	// Delete removes the object under key; missing keys are not an error.
	Delete(key string) error
	// Exist reports whether an object is stored under key.
	Exist(key string) (bool, error)
	// Url returns a URL from which the object can be retrieved.
	Url(key string, opts URLOptions) (string, error)
	// Size returns the stored byte size of the object under key.
	Size(key string) (int64, error)
}

Service is the storage-backend seam. DiskService is the built-in local implementation. The interface is deliberately key-addressed and streaming — upload/download take a key and an io stream, ranged reads go through DownloadChunk, and URLs are minted from a key — so cloud services (S3, GCS, Azure, a mirror service) can implement the same contract without leaking any filesystem assumptions.

type Signer

type Signer interface {
	Sign(data, purpose string) (string, error)
	Verify(token, purpose string) (string, error)
}

Signer is the message-signing seam. In Rails a blob's signed_id is produced by ActiveSupport::MessageVerifier; hosts can plug their own verifier here so signed ids interoperate with an existing application. The purpose argument scopes a token (ActiveStorage uses "blob_id") so a token minted for one purpose cannot be replayed for another.

type Symbol

type Symbol string

Symbol marks a value that is a Ruby Symbol (e.g. a variation's :format value or :gravity argument), as distinct from a String. The distinction is invisible in JSON — both encode to a JSON string — but it changes the bytes of a Marshal.dump, and therefore a variation's digest, so it is preserved here.

type Transformer

type Transformer interface {
	// Transform applies transformations to the image in src, encoding the result
	// as format (e.g. "png", "jpg"), and writes it to dst.
	Transform(dst io.Writer, src io.Reader, transformations Hash, format string) error
}

Transformer is the image-processing seam. Active Storage delegates the actual pixel work to ImageProcessing (libvips / ImageMagick); this library keeps that out of the CGO-free core and lets a host inject a transformer (e.g. one backed by go-images). A variation's descriptor — its key, digest, filename, and content type — is computed without ever calling this.

type URLOptions

type URLOptions struct {
	ExpiresIn   time.Duration
	Filename    Filename
	ContentType string
	Disposition string // "inline" or "attachment"
}

URLOptions carries the keyword options Rails passes to Service#url — how long a generated URL stays valid, and the Content-Disposition/type the download should present.

type Upload

type Upload struct {
	Filename    string
	ContentType string
	Reader      io.Reader
}

Upload is an attachable wrapping an io stream plus its filename and content type — the Rails { io:, filename:, content_type: } attachable form. Attaching one creates and uploads a new blob.

type Variant

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

Variant is a specific transformation of an image blob — ActiveStorage::Variant. Its Key locates the transformed object on the service; producing the bytes needs a Transformer.

func (*Variant) ContentType

func (v *Variant) ContentType() string

ContentType returns the variant's content type (its variation's output type).

func (*Variant) Filename

func (v *Variant) Filename() Filename

Filename returns the variant's filename — the blob's base name with the variation's (downcased) format as extension, per ActiveStorage::Variant#filename.

func (*Variant) Key

func (v *Variant) Key() (string, error)

Key returns the variant's combined service key — "variants/<blob.key>/<SHA-256 hexdigest of the variation key>", matching ActiveStorage::Variant#key.

func (*Variant) Process

func (v *Variant) Process() error

Process downloads the blob, runs the configured Transformer over it, and uploads the transformed bytes under the variant's key — the work ActiveStorage::Variant#process performs. It returns ErrNotTransformable when no Transformer is configured. The digest/key descriptor records never require this.

func (*Variant) Processed

func (v *Variant) Processed() (bool, error)

Processed reports whether the variant's transformed object already exists on the service (ActiveStorage::Variant#processed?).

func (*Variant) Variation

func (v *Variant) Variation() *Variation

Variation returns the variant's variation descriptor.

type VariantRecord

type VariantRecord struct {
	ID              int64
	BlobID          int64
	VariationDigest string
}

VariantRecord mirrors ActiveStorage::VariantRecord — the row that tracks a materialized variant of a blob by its variation digest, pointing (through an attachment named "image") at the variant's own stored blob.

func NewVariantRecord

func NewVariantRecord(blobID int64, variation *Variation) (*VariantRecord, error)

NewVariantRecord builds the tracking record for variation on blobID, computing the variation digest the way Active Storage does.

type Variation

type Variation struct {
	Transformations Hash
	// contains filtered or unexported fields
}

Variation is a set of image transformations that, applied to a blob, yields a variant — ActiveStorage::Variation. The transformations are an ordered Hash (Active Storage deep-symbolizes them); order is preserved because it fixes the bytes of both the signed key and the Marshal-based digest.

func NewVariation

func NewVariation(v Verifier, transformations Hash) *Variation

NewVariation builds a Variation from transformations, signed by v (needed for Key; Digest does not use it).

func (*Variation) ContentType

func (v *Variation) ContentType() string

ContentType returns the MIME type of the variation's output format, matching ActiveStorage::Variation#content_type (Marcel::MimeType.for(extension: format)).

func (*Variation) Digest

func (v *Variation) Digest() (string, error)

Digest returns the variation's digest — the value stored in active_storage_variant_records.variation_digest to look a variant up. It is OpenSSL::Digest::SHA1.base64digest(Marshal.dump(transformations)), reproduced byte-for-byte via the internal Ruby-Marshal encoder.

func (*Variation) Format

func (v *Variation) Format() string

Format returns the output format symbol, defaulting to "png" as Active Storage does when the transformations carry no :format.

func (*Variation) Key

func (v *Variation) Key() (string, error)

Key returns the signed key that identifies the variation in a URL or in a variant's combined key — ActiveStorage::Variation#key, i.e. verifier.generate(transformations, purpose: :variation).

type Verifier

type Verifier interface {
	// Generate returns a signed token binding value to purpose, optionally
	// expiring at expiresAt (pass the zero time for no expiry).
	Generate(value any, purpose string, expiresAt time.Time) (string, error)
	// Verified checks a token's signature, purpose, and expiry, and returns the
	// serialized (JSON) form of the original value.
	Verified(token, purpose string) ([]byte, error)
}

Verifier is the message-verifier seam for the parts of Active Storage whose payloads are typed values rather than opaque strings — a blob's signed_id (an integer id), a variation key (a transformations Hash), and the disk service's direct-upload/blob-key tokens (an ordered Hash). It mirrors ActiveSupport::MessageVerifier#generate / #verified with a purpose and an optional expiry, serializing the value the way Rails does. A Signer that also implements Verifier (RailsVerifier does) is used through this richer interface where a byte-exact wire format matters.

Jump to

Keyboard shortcuts

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