Ryan Endacott

Upload Files to Database in Rails 4 Without Paperclip

While the Paperclip gem is awesome for most Rails use cases, it doesn't have support for saving files to a database. In some scenarios, access to the filesystem or an external service like Amazon S3 isn't feasible. Or maybe you just want to put your files in the database.

As it happens, file upload in vanilla Rails is simple.

First, create a model that will store the file. Give it the following attributes:

  • filename:string
  • content_type:string
  • file_contents:binary

Note: To quickly scaffold the model to save some keystrokes, do rails g scaffold document filename:string content_type:string file_contents:binary in your terminal.

Here's the code for the model and migration:

# app/models/document.rb
class Document < ActiveRecord::Base
end

# db/migrate/20140602005921_create_documents.rb
class CreateDocuments < ActiveRecord::Migration
  def change
    create_table :documents do |t|
      t.string :filename
      t.string :content_type
      t.binary :file_contents

      t.timestamps
    end
  end
end

Next, add a file input to the model's form.

<!-- app/views/documents/_form.html.erb -->
<%= form_for(@document) do |f| %>
  <div class="field">
    <%= f.file_field :file %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Next, you need to make sure you allow the file as a parameter so you can use it in the model.

# app/controllers/documents_controller.rb
def document_params
  params.require(:document).permit(:file)
end

Finally, update the model to read and save the file. In Rails, a file input is passed in as an UploadedFile that can be treated like a normal file. I prefer to save the file in the model initialization.

# app/models/document.rb
def initialize(params = {})
  file = params.delete(:file)
  super
  if file
    self.filename = sanitize_filename(file.original_filename)
    self.content_type = file.content_type
    self.file_contents = file.read
  end
end
private
  def sanitize_filename(filename)
    # Get only the filename, not the whole path (for IE)
    # Thanks to this article I just found for the tip: http://mattberther.com/2007/10/19/uploading-files-to-a-database-using-rails
    return File.basename(filename)
  end

So now you have an application that users can upload files to, but they still can't download them. We can make the show action of the documents_controller download the file. To do so, simply send the file data saved in the document.

def show
  send_data(@document.file_contents,
            type: @document.content_type,
            filename: @document.filename)
end

You've now seen everything it takes to make file upload and download work in vanilla Rails! If you want to do validations on attributes like file size, you'll need to make a slight change to the initialization in the model so the file object can be accessed in the validations.

# app/models/document.rb

validate :file_size_under_one_mb

def initialize(params = {})
  # File is now an instance variable so it can be
  # accessed in the validation.
  @file = params.delete(:file)
  super
  if @file
    self.filename = sanitize_filename(@file.original_filename)
    self.content_type = @file.content_type
    self.file_contents = @file.read
  end
end

NUM_BYTES_IN_MEGABYTE = 1048576
def file_size_under_one_mb
  if (@file.size.to_f / NUM_BYTES_IN_MEGABYTE) > 1
    errors.add(:file, 'File size cannot be over one megabyte.')
  end
end

I've created an example file upload application showing everything in action together. The source is on GitHub.

If you enjoyed this article, follow me on Twitter for more like it. Happy hacking!