Uploading files with Shrine in Hanami
by Paweł Świątkowski
04 Feb 2016
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.