Dis is a content-addressable store for file uploads in your Rails app.
Data can be stored either on disk or in the cloud — anywhere Fog can connect to.
It doesn't do any processing, but provides a foundation for building your own. If you're looking to handle image uploads, check out DynamicImage. It's built on top of Dis and handles resizing, cropping and more on demand.
- Ruby >= 3.2
- Rails >= 7.1
Add the gem to your Gemfile and run bundle install:
gem "dis"Now, run the generator to install the initializer:
bin/rails generate dis:installBy default, files will be stored in db/dis. Edit
config/initializers/dis.rb to change the path or add
additional layers. Cloud storage requires the corresponding
Fog gem:
gem "fog-aws"Run the generator to create your model.
bin/rails generate dis:model DocumentThis will create a model along with a migration.
Here's what your model might look like. Dis does not validate any data by default, but you can use standard Rails validators. A presence validator for data is also provided.
class Document < ActiveRecord::Base
include Dis::Model
validates_data_presence
validates :content_type, presence: true, format: /\Aapplication\/(x\-)?pdf\z/
validates :filename, presence: true, format: /\A[\w_\-\.]+\.pdf\z/i
validates :content_length, numericality: { less_than: 5.megabytes }
endTo save your document, set the file attribute. This extracts
content_type and filename from the upload automatically.
document_params = params.require(:document).permit(:file)
@document = Document.create(document_params)You can also assign data directly, but you'll need to set
content_type and filename yourself:
Document.create(data: File.open("document.pdf"),
content_type: "application/pdf",
filename: "document.pdf")...or even a string:
Document.create(data: "foo", content_type: "text/plain", filename: "foo.txt")Reading the file back out:
class DocumentsController < ApplicationController
def show
@document = Document.find(params[:id])
if stale?(@document)
send_data(@document.data,
filename: @document.filename,
type: @document.content_type,
disposition: "attachment")
end
end
endThe underlying storage consists of one or more layers. Each layer targets either a local path or a cloud provider like Amazon S3 or Google Cloud Storage.
There are three types of layers:
- Immediate layers are written to synchronously during the request cycle.
- Delayed layers are replicated in the background using ActiveJob.
- Cache layers are bounded, immediate layers with LRU eviction. They act as both a read cache and an upload buffer.
Reads are performed from the first available layer. On a miss, the file is backfilled from the next layer.
A typical multi-layer configuration has a local layer first and an Amazon S3 bucket second. This gives you an on-disk cache backed by cloud storage. Additional layers can provide fault tolerance across regions or providers.
# config/initializers/dis.rb
# Fast local layer (immediate, synchronous writes)
Dis::Storage.layers << Dis::Layer.new(
Fog::Storage.new(provider: "Local", local_root: Rails.root.join("db/dis")),
path: Rails.env
)
# Cloud layer (delayed, replicated via ActiveJob)
Dis::Storage.layers << Dis::Layer.new(
Fog::Storage.new(
provider: "AWS",
aws_access_key_id: ENV["AWS_ACCESS_KEY_ID"],
aws_secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]
),
path: "my-bucket",
delayed: true
)Layers can be configured as read-only — useful for reading from staging or production while developing locally, or when transitioning away from a provider.
A cache layer provides bounded local storage with automatic eviction. Files are evicted in LRU order, but only after they have been replicated to at least one non-cache writeable layer. This ensures unreplicated uploads are never lost.
The cache size is a soft limit: the cache may temporarily exceed it if no files are safe to evict, and will shrink back once delayed replication jobs complete.
Dis::Storage.layers << Dis::Layer.new(
Fog::Storage.new(provider: "Local", local_root: Rails.root.join("tmp/dis")),
path: Rails.env,
cache: 1.gigabyte
)Cache layers cannot be combined with delayed or readonly.
You can also interact with the store directly.
file = File.open("foo.txt")
hash = Dis::Storage.store("documents", file) # => "8843d7f92416211de9ebb963ff4ce28125932878"
Dis::Storage.exists?("documents", hash) # => true
Dis::Storage.get("documents", hash).body # => "foobar"
Dis::Storage.delete("documents", hash) # => trueSee the generated documentation on RubyDoc.info
Copyright 2014-2026 Inge Jørgensen. Released under the MIT License.