File Uploads with Gemma
Preview ecosystem guide: Gemma is not part of the Amber 2.0.0-beta.2 core web-app release gate. Its package version, API, and platform support may change independently. Do not add a personal fork as a default dependency.
Gemma is a file attachment toolkit for Crystal applications, inspired by Shrine for Ruby. It provides a clean API for attaching files to your models with support for multiple storage backends.
Why Gemma?
- Storage Agnostic - Switch between filesystem and S3 without changing application code
- Grant Integration - First-class support for Grant ORM with
has_one_attachedandhas_many_attached - Validation Support - Built-in validators for file size, content type, and dimensions
- Plugin System - Extend functionality with MIME type detection, metadata extraction, and more
- Two-Stage Uploads - Cache files temporarily, then promote to permanent storage
Installation
Add Gemma to your shard.yml:
dependencies:
gemma:
github: amberframework/gemma
version: ~> 0.6.5
Run shards install.
Quick Start
1. Configure Storage
# config/initializers/gemma.cr
require "gemma"
Gemma.configure do |config|
# Temporary storage for uploads in progress
config.storages["cache"] = Gemma::Storage::FileSystem.new(
"uploads",
prefix: "cache"
)
# Permanent storage for completed uploads
config.storages["store"] = Gemma::Storage::FileSystem.new("uploads")
end
2. Add Attachment to Model
require "gemma/grant"
class User < Grant::Base
include Gemma::Grant::Attachable
column id : Int64, primary: true
column name : String
column avatar_data : JSON::Any?
has_one_attached :avatar
end
3. Use in Controller
class UsersController < ApplicationController
def create
user = User.new(user_params)
# Assign uploaded file
if file = params.files["avatar"]?
user.avatar = file.file
end
if user.save
redirect_to "/users/#{user.id}"
else
render "users/new.ecr"
end
end
end
4. Display in View
<% if user.avatar %>
<img src="<%= user.avatar_url %>" alt="Avatar">
<% end %>
How It Works
Gemma uses a two-stage upload process:
- Cache Stage - Files are first uploaded to temporary "cache" storage
- Store Stage - On model save, cached files are promoted to permanent "store" storage
This approach provides several benefits:
- Failed validations don't leave orphaned files
- Users can preview uploads before final submission
- Background processing can happen between stages
# Behind the scenes
user.avatar = uploaded_file # Uploaded to cache
user.save # Promoted to store
Core Concepts
UploadedFile
Represents an uploaded file with metadata:
uploaded_file = user.avatar
uploaded_file.id # => "abc123.jpg"
uploaded_file.url # => "/uploads/abc123.jpg"
uploaded_file.size # => 12345
uploaded_file.mime_type # => "image/jpeg"
uploaded_file.original_filename # => "photo.jpg"
uploaded_file.extension # => "jpg"
uploaded_file.exists? # => true
# Access raw IO
uploaded_file.open do |io|
# Process file content
end
# Download to tempfile
uploaded_file.download do |tempfile|
# Work with local file
end
Storages
Gemma supports multiple storage backends:
| Storage | Use Case |
|---------|----------|
| FileSystem | Local development, simple deployments |
| S3 | Production, cloud deployments |
| Memory | Testing |
Attacher
The internal mechanism that manages file attachment lifecycle:
attacher = user._avatar_attacher
attacher.file # Current file
attacher.cached? # File in temporary storage?
attacher.stored? # File in permanent storage?
attacher.changed? # File was modified?
attacher.url # File URL
Features
Single File Attachments
class User < Grant::Base
include Gemma::Grant::Attachable
column avatar_data : JSON::Any?
has_one_attached :avatar
end
user.avatar = File.open("photo.jpg")
user.save
user.avatar_url # => "/uploads/abc123.jpg"
Multiple File Attachments
class Post < Grant::Base
include Gemma::Grant::Attachable
column images_data : JSON::Any?
has_many_attached :images
end
post.images = [File.open("img1.jpg"), File.open("img2.jpg")]
post.save
post.images.each do |image|
puts image.url
end
# Add single file
post.add_image(File.open("img3.jpg"))
# Remove file
post.remove_image(post.images.first)
# Clear all
post.clear_images
Custom Uploaders
Create custom uploaders for specialized handling:
class ImageUploader < Gemma
def generate_location(io, metadata, context, **options)
name = super(io, metadata, **options)
# Organize by model and ID
File.join(
context[:model].class.name.underscore,
context[:model].id.to_s,
name
)
end
end
# Use custom uploader
has_one_attached :avatar, uploader: ImageUploader
Next Steps
- Attachments - Single and multiple file attachments
- Storage Backends - Configure FileSystem and S3
- Validation - Validate file uploads