active_storage_cr
Version, currently 0.1.35 versions
github.com/OrelSokolov/active_storage.cr
No description declared in shard.yml.
Installation
# Add this to your shard.yml
dependencies:
active_storage_cr:
github: OrelSokolov/active_storage.cr
version: ~> 0.1.3Then run:
shards installshard.yml
- Crystal
>= 1.21.0
Dependencies
Runtime Dependencies
- marcel_cr~> 2.1.0github: OrelSokolov/marcel.cr
- action_pack_cr>= 0.1.0github: OrelSokolov/action_pack.cr
- attr_reader_cr~> 0.1.0github: OrelSokolov/attr_reader.cr
- active_record_cr~> 0.1.0github: OrelSokolov/active_record.cr
- active_support_cr~> 0.1.0github: OrelSokolov/active_support.cr
- image_processing_cr~> 1.14.0github: OrelSokolov/image_processing.cr
README
active_storage.cr
A 1:1 Crystal port of activestorage 8.1.4 (the Rails engine) under the
same ActiveStorage namespace: has_one_attached/has_many_attached on
ActiveRecord models, Disk and S3/MinIO services, image variants (via the
vips CLI), Marcel-backed blob identification, blob analysis
(Blob#analyze/analyze_later — width/height from images through
vips/MiniMagick, duration/rotation from video/audio via ffprobe,
NullAnalyzer fallback), and Rails-compatible
signed_id tokens
(urlsafe_b64_nopad(...) + "--" + hmac_sha256-hex, byte-for-byte against
the Rails 8.1 verifier). The S3 client is AWS SigV4 on OpenSSL +
HTTP::Client — no SDK. Suite: 134/134 specs replaying
27 + 19 + 16 + 37 + 21 oracle cases recorded from the real gem
(+ identify cases verified against the Ruby marcel gem).
⚠️ EXPERIMENTAL FRAMEWORK, DO NOT USE IN PRODUCTION YET.
Installation
dependencies:
active_storage_cr:
github: OrelSokolov/active_storage.cr
shards install
Depends on active_support.cr, active_record.cr, marcel.cr
(blob identification, format/content-type of variations) and
image_processing.cr (variant processing — variant_processor = :vips
needs libvips, :mini_magick/default needs ImageMagick 7); the direct-upload
HTTP layer additionally needs action_pack.cr (opt-in require, not pulled
in by the umbrella active_storage_cr).
Usage
require "active_storage_cr"
ActiveStorage.service = ActiveStorage::DiskService.new("/srv/storage")
class User < ActiveRecord::Base
has_one_attached :avatar
has_many_attached :photos
end
user.avatar.attach(File.read("/tmp/avatar.png"), "avatar.png", "image/png")
user.avatar.attached? # => true
user.avatar.download # => png bytes
user.avatar.variant(resize_to_limit: [100, 100], format: "webp").url
# has_many_attached: io triplets, existing blobs, or direct-upload signed ids
user.photos.attach(File.read("/tmp/1.jpg"), "1.jpg", "image/jpeg")
user.photos.attach([blob, other_blob])
user.photos.attach(blob.signed_id)
ActiveStorage::Blob.find_signed!(signed_id) # raises on bad signature
# S3/MinIO (path-style, header signing, presigned GET/PUT):
ActiveStorage.service = ActiveStorage::S3Service.new(
endpoint: "http://127.0.0.1:9000", bucket: "photos",
access_key_id: "minioadmin", secret_access_key: "minioadmin")
# Direct upload controllers (opt-in; pulls action_pack):
require "active_storage_cr/controllers"
Feature comparison vs activestorage 8.1.4
DSL & attachables
| Feature | Original (activestorage) | Port (active_storage.cr) | Status |
|---|---|---|---|
has_one_attached / has_many_attached macros | ✅ | Macros on ActiveRecord::Base; avatar, avatar_blob, photos_attachments, photos_blobs | ✅ |
attach with io/filename/content_type | ✅ (IO objects) | Same, io is a String of bytes | ⚠️ partial |
attach with Blob / signed_id / arrays | ✅ | Attached::Many only (Blob, signed_id, arrays, triplets); Attached::One io-form only | ⚠️ partial |
attach(ActionDispatch::Http::UploadedFile) | ✅ | n/a in Crystal | ❌ |
attached?, detach, purge, download | ✅ | Same (detach keeps blobs, purge removes them) | ✅ |
purge_later | ✅ | Not ported | ❌ |
with_attached_* scopes, avatar= setter | ✅ | Not ported | ❌ |
Attachment order = insertion (created_at, id) | ✅ | Same | ✅ |
Blobs, signed ids & services
| Feature | Original | Port | Status |
|---|---|---|---|
Blob: byte_size, content_type, checksum, key, image?/video?/audio?/text? | ✅ | Same | ✅ |
Filename sanitization (basename/extension) | ✅ | Same | ✅ |
signed_id / find_signed! / find_signed (InvalidSignature vs RecordNotFound) | ✅ | Same wire format (both data and legacy message envelopes) | ✅ |
| Disk service | ✅ | DiskService | ✅ |
| S3 service (upload/download/delete, Content-MD5 integrity, presigned GET) | ✅ (aws-sdk-s3) | S3Service with SigV4 on OpenSSL, no SDK | ✅ |
config/storage.yml registry, per-blob service (service_name) | ✅ | Single global ActiveStorage.service (name kept in tokens, not dispatched) | ❌ |
Mirror service | ✅ | Not ported | ❌ |
custom_metadata, analyzed?/AnalyzeJob | ✅ | Not ported | ❌ |
Blob identification (before_save :identify, identified?) | ✅ (marcel) | marcel_cr: magic bytes > filename > declared type; create_and_upload! unfurl semantics | ✅ |
Variants & representations
| Feature | Original | Port | Status |
|---|---|---|---|
variant(resize_to_limit:/resize_to_fill:/resize_and_pad:/crop:/rotate:/format:/…) | ✅ (image_processing) | Any image_processing op through ActiveStorage::Transformers (variant_processor: :mini_magick default / :vips / :disabled, as in 8.1.4) | ✅ |
#url, #processed (lazy + cache by variation key) | ✅ | Same; key shape variants/{key}/[0-9a-f]{64} matches | ✅ |
| Variation key derivation | HMAC-verifier + SHA256 | SHA256 of canonical JSON (same shape, different bytes) | ⚠️ partial |
preview (ffmpeg/poppler), representation | ✅ | Not ported | ❌ |
Direct upload & HTTP routes
| Feature | Original | Port | Status |
|---|---|---|---|
Blob.create_before_direct_upload!, service_url_for_direct_upload, service_headers_for_direct_upload | ✅ | Same | ✅ |
| S3 presigned PUT (query-SigV4, UNSIGNED-PAYLOAD) | ✅ | Same format as aws-sdk (oracle-verified) | ✅ |
DirectUploadsController (JSON 1:1) + DiskController (204/404/422, inline send_file) | ✅ | src/controllers.cr, opt-in require; routes via CRails::Routes::Drawer#active_storage | ✅ |
/rails/active_storage/blobs/{redirect,proxy}, representations routes | ✅ | Not ported | ❌ |
Deviations & Crystal-specific notes
- Single global
ActiveStorage.serviceinstead of the gem's per-blob service registry (storage.yml+config.active_storage.serviceare not implemented). - Disk URLs are relative; the base comes from
ActiveStorage.disk_url_base(Rails builds absolute URLs fromActiveStorage::Current.url_options). - The verifier secret is
CRAILS_SECRET_KEY_BASEwith a fixed development fallback (Rails usessecret_key_base). - Variation key is SHA256 of canonical JSON, not Rails' signed HMAC+SHA256 (Rails-internal); key shape is identical.
- Direct-upload controllers are an opt-in
requireso the shard doesn't pull action_pack transitively; namespace-compatible dispatch ("active_storage/disk") works. - The AR port writes
created_at/updated_atunconditionally, so the blobs/attachments tables carryupdated_at(Rails' tables don't). - Attachables are typed as unions/overloads (String io,
Blob,signed_id);io:is aStringof bytes, not an IO object. - Analyzer port: analyzers download the blob to a temp file and work on
its PATH;
Blob#analyze_laterperforms inline unless a job backend is wired through the opt-insrc/analyze_job.cr(active_job_cr);AnalyzeJobcarries the blob id, not a GlobalID. The gem's Attachmentafter_create_commit :analyze_blob_laterhook is not ported — apps callblob.analyze!themselves. The gem's metadata-storedidentifiedflag is not written (the port'sidentifypredates it), so stored metadata lacks"identified":true. - ImageMagick analyzer shells out to
identify -format "%[orientation]"directly (image_processing.cr's MiniMagick has no orientation reader).
Full list with rationale: PLAN.md (Russian).
Testing
crystal spec # 113/113 — needs local PostgreSQL (db crails_as_test)
The S3 group additionally needs MinIO on 127.0.0.1:9000 (buckets
as-oracle, as-crystal); without it those specs skip with a notice.
Oracle cases recorded from activestorage 8.1.4: 27 core
(oracle/cases.rb), 19 has_many_attached (cases_many.rb), 16
signed_id (cases_signed_id.rb), 37 S3/direct-upload against live MinIO
(cases_s3.rb) — replayed 1:1 in spec/.
Related
- CRails siblings:
active_record.cr,active_support.cr,action_pack.cr— this shard is the ActiveStorage piece of the stack.
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.1.3- Tagged
- Oct 20, 2026
- Commit
90fbd9db7f47- Crystal
>= 1.21.0- Indexed
- yes
Dependents
Repository
github.com/OrelSokolov/active_storage.cr
Metadata
- Created
- Oct 14, 2026
- Updated
- Oct 24, 2026
- Synced
- Oct 24, 2026
- Versions
- 5