Project

dis

0.0
No release in over a year
Dis is a Rails plugin that stores your file uploads and other binary blobs.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 2.1.2, < 2.7.0
>= 7.1
 Project Readme

Version Build

Dis

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.

Requirements

  • Ruby >= 3.2
  • Rails >= 7.1

Installation

Add the gem to your Gemfile and run bundle install:

gem "dis"

Now, run the generator to install the initializer:

bin/rails generate dis:install

By 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"

Usage

Run the generator to create your model.

bin/rails generate dis:model Document

This 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 }
end

To 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
end

Layers

The 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.

Cache layers

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.

Low-level API

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)       # => true

Documentation

See the generated documentation on RubyDoc.info

License

Copyright 2014-2026 Inge Jørgensen. Released under the MIT License.