Add Latch to your shard.yml and run shards install:
dependencies:
latch:
github: wout/latch
Then require it from src/shards.cr. The require you pick determines
which framework integrations are loaded:
# src/shards.cr
require "avram"
require "lucky"
require "carbon"
require "authentic"
require "latch"
require "latch/lucky/avram" # Lucky + Avram (the default for new Lucky apps)
If you only need Lucky without Avram (for example, in an API-only app that streams uploads straight to S3), use the lighter integration:
require "latch/lucky/uploaded_file"
And if you use Avram outside of Lucky:
require "latch/avram/model"
Latch routes every upload through a named storage backend. By convention
there are two: "cache" for temporary uploads (the file as it sits between
a form submission and a successful save) and "store" for permanent
storage. Configure both in a new file at config/latch.cr:
# config/latch.cr
Latch.configure do |settings|
settings.storages["cache"] = Latch::Storage::FileSystem.new(
directory: "uploads", prefix: "cache"
)
settings.storages["store"] = Latch::Storage::FileSystem.new(
directory: "uploads"
)
settings.path_prefix = ":model/:id/:attachment"
end
The path_prefix controls where files end up inside a storage backend. The
placeholders above expand to, for example, user/42/avatar/a1b2c3d4.jpg.
You can use any combination of :model, :id, and :attachment, or omit
path_prefix entirely if you’d prefer the default flat layout.
By default the
FileSystemstorage writes to a directory inside your project root. If you want files to be served by Lucky’s static handler, pointdirectoryatpublic/uploadsinstead.
Latch ships with three storage backends out of the box.
Useful for development, single-server deployments, and anywhere you want files written to local disk:
Latch::Storage::FileSystem.new(
directory: "uploads",
prefix: "cache", # optional subdirectory
clean: true, # remove empty parents on delete
permissions: File::Permissions.new(0o644),
directory_permissions: File::Permissions.new(0o755)
)
Use the S3 backend for production deployments, or for any S3-compatible service such as Cloudflare R2, Tigris, or RustFS (the open-source successor to MinIO):
Latch::Storage::S3.new(
bucket: "my-bucket",
region: "eu-west-1",
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
endpoint: "http://localhost:9000", # optional, for S3-compatible services
prefix: "uploads", # optional key prefix
public: false, # set to true for public-read ACL
upload_options: {
"Cache-Control" => "max-age=31536000",
}
)
The S3 backend depends on the awscr-s3 shard, so add it to your
shard.yml alongside Latch:
dependencies:
latch:
github: wout/latch
awscr-s3:
github: taylorfinnell/awscr-s3
Once configured, you can generate presigned URLs from any stored file:
user.avatar.try(&.url(expires_in: 1.hour))
The in-memory backend is intended for tests. It keeps everything in process, so there are no files to clean up between specs:
Latch::Storage::Memory.new(base_url: "https://cdn.example.com")
See Working with the test environment below for the recommended setup.
A typical Lucky app uses different backends in development, test, and
production. The cleanest way to express that is to switch on
Lucky::Env:
# config/latch.cr
Latch.configure do |settings|
if LuckyEnv.test?
settings.storages["cache"] = Latch::Storage::Memory.new
settings.storages["store"] = Latch::Storage::Memory.new
elsif LuckyEnv.production?
settings.storages["cache"] = Latch::Storage::S3.new(
bucket: ENV.fetch("S3_BUCKET"),
region: ENV.fetch("AWS_REGION"),
access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID"),
secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY"),
prefix: "cache"
)
settings.storages["store"] = Latch::Storage::S3.new(
bucket: ENV.fetch("S3_BUCKET"),
region: ENV.fetch("AWS_REGION"),
access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID"),
secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY"),
prefix: "uploads"
)
else
settings.storages["cache"] = Latch::Storage::FileSystem.new(
directory: "uploads", prefix: "cache"
)
settings.storages["store"] = Latch::Storage::FileSystem.new(
directory: "uploads"
)
end
settings.path_prefix = ":model/:id/:attachment"
end
With the in-memory backend in place, every spec starts with empty storage.
To make sure no file leaks between examples, clear the storages in a
Spec.before_each hook:
# spec/spec_helper.cr
Spec.before_each do
Latch.settings.storages.each_value(&.as(Latch::Storage::Memory).clear!)
end
Custom storages, for example an S3-compatible service running locally, can be plugged in the same way. See Custom storage in the Latch README for the storage interface.
With Latch installed and configured, you’re ready to define an uploader: the struct that describes how a particular kind of file is stored and what metadata it carries.