Uploading files with Shrine in Hanami

In almost every web application there is a set of features that are standard and common. Take user registration and authentication, for example. Or commenting system. There’s a pretty big chance you’ll be needing one of these in majority of the applications you create, especially when providing information is one of their goals. Another example of such features is file uploading.

When programming in Rails we are lucky enough that the ecosystem can handle the hassle for us. We have Devise or Sorcery for authentication, Pundit or CanCanCan for authorization, Paperclip or Carrierwave (or even Refile) for file uploading etc. They make less or more assumptions but, unless you are targeting some very specific requirements, they have a huge upside: they work.

Shrine is a relatively new kid in the uploading block. So why bother? Isn’t it like another JavaScript framework or PHP CMS? I’ll go with a “no”. Shrine is written differently. It does not target Rails as a first-class citizen – it’s written in plain Ruby. That makes it a perfect candidates for other Ruby web frameworks, like Hanami (previously known as Lotus). And although writing your own file uploader is not exactly a rocket science, you might consider using Shrine with a plugin hanami-shrine written by me.

Let’s start, then

I suppose you have already generated a scaffold of new Hanami application (type hanami new hanami-shrine now, if you haven’t), configured database etc. Let’s add a model, repository and a migration to it:

# lib/hanami-shrine/entities/image.rb

class Image
  include Hanami::Entity

  attributes :title, :image_data
end

# lib/hanami-shrine/repositories/image_repository.rb

class ImageRepository
  include Hanami::Repository
end

# db/migrations/somedatehere_create_image.rb

Hanami::Model.migration do
  change do
    create_table :images do
      primary_key :id
      column :title, String
      column :image_data, String
    end
  end
end

This should all be familiar to you, should you have done any Hanami development before. We also need to add mapping:

mapping do
  collection :images do
    entity Image
    repository ImageRepository

    attribute :id,          Integer
    attribute :title,       String
    attribute :image_data,  String
  end
end

Now, let’s add Shrine part. First of all, Gemfile:

gem 'shrine'
gem 'hanami-shrine'

The I prefer adding a kind of initializer in lib/hanami-shrine/image_uploader.rb. You need to include explicit reference to this file on top of the entity and repository (but it’s good to explicitly state dependencies anyway). The contents of the file is Shrine as described in its README. You can leave out validations: true if you don’t plan to use it.

require 'shrine'
require 'shrine/storage/file_system'

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new(Dir.tmpdir),
  store: Shrine::Storage::FileSystem.new("public", subdirectory: "uploads")
}

class ImageUploader < Shrine
  plugin :hanami, validations: true
end

We also need to alter our entity and repository according to hanami-shrine README:

# entity

require_relative '../image_uploader'

class Image
  include Hanami::Entity
  include ImageUploader[:image]

  attributes :title, :image_data
end

# repository

require_relative '../image_uploader'

class ImageRepository
  include Hanami::Repository
  extend ImageUploader.repository(:image)
end

We are sut up now, so let’s create some controllers. Controller for new action is boring, so I’ll only show the one for create:

# apps/web/controllers/images/create.rb

module Web::Controllers::Images
  class Create
    include Web::Action

    def call(params)
      tempfile = params['image']['tempfile']

      image = Image.new
      image.title = params['title']
      image.image = ::File.open(tempfile)

      image = ImageRepository.create(image)

      redirect_to routes.url(:image, image.id)
    end
  end
end

And the template for new:

# apps/web/templates/images/new.html.erb

<form method="post" action="<%= Web::Routes.url(:images) %>" enctype="multipart/form-data">
  <p><input type="text" name="title" id="title" placeholder="Title" /></p>
  <p><input type="file" name="image" id="image" /></p>
  <p><input type="submit" value="Upload"></p>
</form>

Nothing here is really surprising, just a standard Hanami constructs. And you might be a little surprised, but that’s it. Of course, you might want to create some other actions, like for example show:

<h1><%= image.title %></h1>

<img src="<%= image.image_url %>" />

<p>
  <%= form_for :image, routes.image_path(id: image.id), method: :delete, values: {image: image} do
        submit 'delete'
      end
  %>
</p>

Final words

As I said before, writing your own file upload would not be that difficult, but I suggest you seriously consider using Shrine for that. It gives you a lot of features like validation, file processing etc. with it. And I really would like to see it as a standard. And not only for Hanami, for Rails too.

By the way, I did not show you how to validate the entity. That’s a final bonus for getting that far with reading this blog post:

class ImageUploader < Shrine
  plugin :validation_helpers
  plugin :determine_mime_type
  plugin :hanami, validations: true

  Attacher.validate do
    validate_max_size 180_000, message: "is too large (max is 2 MB)"
    validate_mime_type_inclusion ["image/jpg", "image/jpeg"]
  end
end

Thanks for reading and happy uploading!

PS. You can see the complete code in this repository.

Related posts