Documentation

File Uploads (Gemma)

New
Browse documentation

Published 2026-08-13. V2 is a prerelease; the web core is release-gated and other previews are named separately. What beta means.

Read this page as HTML, Markdown, or structured JSON—or open the published Markdown with an AI assistant. Gemini receives the prompt through your clipboard because its signed-out page does not reliably prefill URL text; paste when the new tab opens. External assistants need the public site URL.

1.4.1
Unavailable in this version
1.5
Unavailable in this version

File Uploads with Gemma

Preview ecosystem guide: Gemma is not part of the Amber 2.0.0-beta.5 core web-app release gate. Its package version, API, and platform support may change independently. Confirm a compatible official release before adding it to an application.

Gemma is a file attachment toolkit for Crystal applications, inspired by Shrine for Ruby. It connects model attachments to validation, temporary uploads, permanent storage, and delivery across configurable backends.

Authored assets and uploads are different lifecycles

Use Asset Pipeline for files that ship with an application release: CSS, JavaScript, logos, interface images, fonts, icons, and other reviewed static files. Those files can be content-addressed, included in a release manifest, and cached immutably because the deployment owns their bytes.

Use Gemma for files received while the application is running. An upload is untrusted input and may be private, replaced, or deleted. Do not copy uploads into the Asset Pipeline source tree, add them to its manifest, or assume its immutable cache policy applies. Validate the file, store it outside the application release artifact, and choose an authenticated controller response, a presigned object-storage URL, or an explicitly configured public-upload route for delivery.

Where the examples go

  • Add dependencies in shard.yml and run commands from the application root.
  • Configure Gemma in config/uploads.cr. The generated application entry point loads top-level config/* files before application source.
  • Attachment declarations belong in Grant models under src/models/.
  • Upload handling belongs in the receiving controller under src/controllers/; form and display markup belongs in the matching ECR file under src/views/.

Blocks on this page use those destinations unless a closer label says otherwise.

Why Gemma?

  • Storage Agnostic - Switch between filesystem and S3 without changing application code
  • Grant Integration - First-class support for Grant ORM with has_one_attached and has_many_attached
  • Validation Support - Built-in validators for file size, content type, and dimensions
  • Plugin System - Add MIME type detection and metadata extraction
  • Two-Stage Uploads - Cache files temporarily, then promote to permanent storage

Installation

File: shard.yml — add Gemma under the existing dependencies: key.

YAML
dependencies:
  gemma:
    github: amberframework/gemma
    version: ~> 0.6.5

Run shards install from the application root.

Quick Start

1. Configure Storage

File: config/uploads.cr — create this complete storage configuration. Do not put it in the generated empty config/initializers/ directory unless you also add and verify an explicit require.

Crystal
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

File: the application entry point, for example src/my_app.cr — retain require "../config/*" before controllers and models. A migrated app with a narrower require list must explicitly require ../config/uploads; creating the file alone does not load it.

This example stores files under project-root uploads/. Amber's generated static route serves public/; it does not make project-root uploads/ public. Keep private uploads there and deliver them through an authorized application endpoint or object storage. If the product deliberately uses public local uploads, configure a dedicated persistent directory and route, and test the returned Gemma URL before rendering it in a view.

2. Add Attachment to Model

File: src/models/user.cr — keep the attachment declaration inside User.

Crystal
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

File: src/controllers/users_controller.cr — add this behavior inside the action that receives the upload.

Crystal
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

File: src/views/users/show.ecr — render the attachment inside the user page.

ECR template
<% if user.avatar %>
  <img src="<%= user.avatar_url %>" alt="Avatar">
<% end %>

This view assumes avatar_url resolves through the delivery path selected above. Request that URL directly during verification; a URL-shaped value alone does not prove that Amber can serve the stored file.

How It Works

Gemma uses a two-stage upload process:

  1. Cache Stage - Files are first uploaded to temporary "cache" storage
  2. 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
Crystal
# Behind the scenes
user.avatar = uploaded_file  # Uploaded to cache
user.save                     # Promoted to store

Core Concepts

UploadedFile

Represents an uploaded file with metadata:

Crystal
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:

Crystal
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

Crystal
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

Crystal
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:

Crystal
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