marten-storages
marten_storages
A Crystal shard for Marten that ports Rails Writebook's Active Storage analog — the blob + attachment + variant pipeline — to Marten.
Rails Writebook builds on Active Storage and patches it via lib/rails_ext/active_storage_sluggable.rb. Marten ships only the low-level :file / :image fields with no equivalent for has_one_attached / has_many_attached, polymorphic attachment tables, or named on-disk variants. This shard fills that gap.
Installation
dependencies:
marten_storages:
github: stevegeek/marten-storages
Then shards install.
The shard depends on crystal-vips for image resizing — make sure libvips is available on your system (brew install vips on macOS).
Usage
1. Define a concrete Attachment row model in your app
Marten's polymorphic to: list is compile-time fixed, so the shard can't ship a usable polymorphic table — your app owns it. The model must declare these fields exactly (names matter; the shard's Service queries them):
class Attachment < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :record, :polymorphic, to: [Book, Picture, Markdown], related: :attachments
field :name, :string, max_size: 64, blank: false, null: false
field :file, :file, blank: false, null: false, upload_to: "attachments"
field :variant_of, :many_to_one, to: Attachment, related: :variants, blank: true, null: true, on_delete: :cascade
field :variation_kind, :string, max_size: 64, blank: true, null: true
field :content_type, :string, max_size: 128, blank: true, null: true
field :byte_size, :big_int, blank: true, null: true
with_timestamp_fields
end
2. Mix Attachable into each target model
class Book < Marten::Model
include MartenStorages::Attachable
field :id, :big_int, primary_key: true, auto: true
field :title, :string
attachment_model ::Attachment
end
This adds attachment_for(name), attachments_for(name) and variants_of(attachment, kind) instance methods.
3. Attach files via Service
# Basic attach:
MartenStorages::Service.attach(
model: ::Attachment,
record: book,
name: "cover",
uploaded_file: uploaded,
)
# Attach + generate variants in one call (libvips resize):
MartenStorages::Service.attach(
model: ::Attachment,
record: picture,
name: "image",
uploaded_file: uploaded,
variants: {"large" => {max_dimension: 1500}},
)
# Lookups:
cover = MartenStorages::Service.find_one(model: ::Attachment, record: book, name: "cover")
thumb = MartenStorages::Service.variant_of(model: ::Attachment, original: cover, kind: "thumbnail")
4. (Optional) Configure shared variant kinds
MartenStorages.configure do |c|
c.default_variant_format = "jpg"
c.register_variant("thumbnail", max_dimension: 600)
c.register_variant("large", max_dimension: 1500)
end
The registry is sugar — Service.attach always accepts inline variants: hashes too.
Why a shard?
Rails Writebook builds on Active Storage and patches it via active_storage_sluggable.rb. The Marten port keeps the same shape — single polymorphic Attachment table + pre-computed variant rows — but lives in its own shard because:
- Marten has no built-in blob/attachment indirection to patch onto.
- The polymorphic
to:list must be compile-time fixed and host-owned. - Variant generation via libvips is general-purpose and worth reusing across any Marten app needing image attachments.
Development
shards install
script/cr spec/ # or `crystal spec`
License
MIT — see LICENSE.
marten-storages
- 0
- 0
- 0
- 0
- 3
- 5 days ago
- May 12, 2026
MIT License
Tue, 12 May 2026 13:48:36 GMT