Skip to main content

50 File Uploads in Rails With Shrine

Episode 142 · September 23, 2016

Add file uploads to your application with the Shrine gem

File Uploading


Transcripts

What's up guys, this episode is a long time coming and we're talking about the Shrine ruby gem for uploading files into your web application, so in the past, you've had Paperclip, Carrierwave, refile, all these different options for uploading files, and Shrine is a new look at all of that, and it comes with a very awesome architecture that allows you to plug and play all kinds of different features nicely into it. It has all of these plugins that you can add in, and you can add whatever features you want, and you can easily build your own plugins if you want custom stuff, like maybe you want to add your own transcoding a video, you can add that as a plugin that you could include into your file uploads. All of this is really really nicely organized and modular, and they also have really cool documentation if you´re interested in upgrading from refile or paperclip, or carrierwave and starting to use Shrine, you can just follow those instructions, and the community behind this is doing a wonderful job of adding in documentation and plugins for pretty much any feature you might expect to have supported in your file upload utility, and that´s really exciting to see. Without further ado, we´re going to take Shrine and set that in a rails application with ActiveRecord, you'll be able to upload files, and we'll print those files out on the screen, so we'll upload images and show them as image tags, and then in a follow up episode we'll go deeper into Shrine, we'll do our own video uploading and processing of that so that we can pull out a thumbnail from a video that you upload and maybe transcode videos and all that fun stuff. Let's just dive into integrating Shrine.

Let's pop over to the GitHub page because some of the documentation is very good, and it's all modular documentation, but if you're adding this to your rails application, it can be a little confusing what steps you actually need to do in order to get this all set up. Let's dive into the README where it has a better example to get started with. The "Quick Start" here is what we're going to look at. First you need to add the gem, then you need to require inside of an initializer somewhere, and you set the cache in the store. The cache is that place where you upload a file, and maybe you upload an avatar but your password wasn't correct, so the avatar gets uploaded, but it doesn't really get saved to the record, because the record wasn't valid, so you would rerender the form with that cached image, and so the cache is one place, and then when it gets permanently stored, it goes into the store. So you need to define those two things, you can save that to the file system, but you can also do some extra things like saving that to Amazon S3 or whatever. This is fully configurable in order to have different locations for the cache and the permanent store, and you could also add your plugins for ActiveRecord, and for example, cache attachment data, which will use that cache when your forms rerender because they fail. You need to do that, and then the next step is you need to create your migration and of course, these are a little harder to follow because they aren't ActiveRecord ones, but they're very similar. You need to add upload data column, so whatever you want to call your upload. In this case it's called image. It's called image_data as a text column, and this will be a json serialized column. When you upload a file, you'll be able to look at that column in your database, and it will be JSON which you can pull out and you can store in anything you want in there, which means you can store metadata, if you upload a video, you can store the video and a thumbnail, so you can include links to both of those in the same upload, which is kind of nice. Then, you pretty much create your uploader and inherit from Shrine, you can add in all your processing that you want. Maybe you want thumbnails, that sort of thing, and then you include the image uploader into your ActiveRecord model or your SQL model, whichever you're using, and you're good. The examples need some customizing for ActiveRecord, but that is awesome because they showcase that this doesn't have to work with rails, which is super nice. Let's follow this and go add that into our own rails application.

First thing is first, let's generate a new rails application. Let's just call this rails new file_uploader and let's cd file_uploader, and let's pretend that we're building an instagram where you have posts, and a post has an image or a video. We're only going to care about uploading images, but we need to generate a scaffold for a model called Post, and more specifically, we're going to need to generate that column for Shrine. Let's do this first before we add Shrine. We're going to need the column for-- They're calling their table photos, and image data :image_data is the column, so let's say we have posts and we have rails g scaffold Post image_data:text we'll create this, and we'll go modify our form, so rather than you submitting text, you will have a file upload field for the image instead. Let's run rake db:migrate, and then let's open up our editor, and if we go into config/routes.rb we can set root to: "posts#index" and that will make that nice, and let's go to the forum as well. You'll see that they generated a text area form input for us because we said it was a text column, but we're actually going to change this once we have Shrine installed, so now let's go do that.

I am going to grab the latest version of Shrine from rubygems. We'll get 2.3.1, and we'll add that to our Gemfile at the bottom. Let's go back to our console and run bundle to install Shrine, and then if we go into Shrine's GitHub README, we can grab all of this code from their example, and we'll put that into our config/initilizer in a new file we'll call shrine.rb

config/initializers/shrine.rb

require "shrine"
require "shrine/storage/file_system"

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"),
}

Shrine.plugin :activerecord
Shrine.plugin :cached_attachment_data # for forms

We want to save this to our file system, in production we want to change that, so we won't want them stored on our local hard drive in, for example if you're on Heroku, you can't store anything like file uploads on your server's hard drive, they force you to use something like S3. In development you're probably always going to want to save it on your own computer for speed, and so we'll do this here but you can go and customize that for production to store to S3, but development can store on your file system, and we'll talk about that later. Just getting up and running, we'll use the file system storages, and we'll also use the cached attachment data just so we can see how this woks. Let's so back and, in order to show the cached attachment data, let's go back to our migrations, and let's say this needs a description for every post, and then if we go into

app/models/post.rb

class Post < ApplicationRecord 
    validates :description, presence: true 
end 

We'll be able to then test those uploads that don't succeed, and this will validate those. That way, we can say if there's no description, it won't succed, and then we can test our cached image upload and make sure that that is working here in a little bit, but first we're going to need to run rake db:rollback in order to remove that databas table, and then we can run rake db:migrate in order to make sure that we get the database table with both of those. The reason why I didn't add a new migration was because this is a brand new app and we haven't deployed this to production, so we don't really need to add a new migration because this is our very first one, so we might as well have clean migrations and just edit that first file while we're getting started instead of adding a bunch of extra migration files in there that would just get messy. Now that we have Shrine installer, we have our database set up, we have the initializer set up, we need to then go and create an uploader, and what I'm going to do is create a folder for this inside app, and we'll call it

app/uploaders/image_uploader.rb

class ImageUploader < Shrine 
end 

We can customize how this works in the future, but right now, as long as you inherit from Shrine, that is all you need to do in order to get the basics to work. This is where you would plug in all of your extra stuff for processing thumbnails or smaller version of your files, you would plug that in here, but we're not going to do it yet, and we'll take a look at that after we get the basics set up. Now we can go into our

app/models/post.rb

class Post < ApplicationRecord 
    include ImageUploader[:image]

    validates :description, presence: true 
end

This is that column that we created called Image data, except for ease of working with this, it's going to be called image, and Shrine will know how to convert that t the image data field on the database level.

Now Shrine is connected on the app database models level, and we need to go modify our form in our controller in order to wire up the form to point to Shrine, and then the controller to accept the image upload from Shrine. Let's take a look at their example. They show using these two inputs here which we can use in order to build ours, and so with rails helpers, you can do something like this. Let's copy those two lines and go into the form_for(posts) and let's paste them in, and what we're looking at is this f.text_area needs to go away and we need to replace that with f.file_field :image the label now should probably point to image, because that's a virtual attribute that Shrine added in our post model, you saw the square brackets Shrine there, so I'll show you that. This name that you give it here is what you can use in your form fields, so this is defining a virtual attribute that will get converted into an upload, and then all of the JSON for it gets stored in the image data which is a real column. With that in mind, we can then take this hidden field and also put that right afterwards, but here we can change @photo to f.object.cached_image_data, and if you haven't used f.object, when you pass in, in a form_for, if you pass in something, an object, like a post, then F, which represents your form and allows you to create form fields, this knows what you passed in, and that is the f.object, so whenever you pass in a post or something like that, this will automatically assign it to f.object, and then you can use that to reference posts as well, so they had a reference to the instance variable, which makes the form a little less reusable, if you were ever to display this on multiple pages, so if you do this, it's all self-contained within the form builder itself, which is just kind of nice to have. If you haven't checked that out before, that's how that works. Cached image data is basically, it's adding-- It knows the name is image, so then it adds _data afterwards and cached_ before, and so based on that attribute name that you put on the model it generates this method which allows you to save that cached image across forms. With that in mind, let's add

<div class="field">
    <%= f.label :description %> 
    <%= f.text_area :description %>
</div>

Now, we should be ready to go run our rails application, and then try out an upload. First thing is first, let's upload a new post. Let's grab an image here, and then let's put in no description and see if this fails to save the post as it should, because we validate that the description has a presence, which if we don't type anything in, it should fail the validation, and you'll see that the image field is empty because we are not-- we haven't chosen a new file, but if we inspect this, we should be able to see that our hidden field should have a value, but it does not, and that is because we have a missed our

controllers/posts_controller.rb

def post_params 
    params.require(:posts).permit(:image)
end 

Now, we're working off of the parameter called image. If we submit this again, it should still fail, but this time, we should see that there's cached image data in the value, and there still is not, but we can take a look at this, so we still have an unpermitted parameter description, so we can take a look at the post controller again, we can add in the description. Now if we go back into the form, there's one thing that I did on purpose out of order just to show you the importance of this. You'll notice that we see the file field first and the hidden field afterwards, and the hidden field sets a value to the cached image data. The way that the browser works is that if you have two fields with the exact same name, which the cached version is the exact same name, one happens to be a file field, the other happens to be a hidden field, but they're equivalent in the browser, they just display differently. The names and the values are the same, or the names of the fields are the same. The trick with this is that the example shows that you need the hidden field first and the file field second, and the reason for that is because the browser will always send the last value on the page for that name. With this, no matter what you would fill out for the file field, it would never send over an image, because the hidden field would overwrite that value with an empty string, because this would always be empty because you never submitted over an image, so this would overwrite it, so you have to be careful to make sure that you put your hidden field first in order to make sure that when you choose a file in the upload input, that that would override any of the cache data. This needs to be first, and it's not as obvious as you might think it is, so that could trip you up if you're not familiar with how that works in the browser. That's also the trick how rails will make check boxes false by default, so they automatically with the check box tag, we'll put in hidden field setting the value to false, and when you check the box it becomes true, and if you don't check the box, it submits the hidden field instead, because the browsers don't, for whatever reason send over an unchecked value of a checkbox at all, so you have to have the hidden field for those. With that change to put this hidden field first, we should not only be able to get the description over, but we should be able to submit our image and create the posts, so when we create the post without a description, it will still fail, but this time, we should be able to inspect and see that the value for the hidden field now has some JSON in it, which references the id in the cache, in the storage cache for the image we just uploaded. So we uploaded that image, but it saved it in the cache because we never successfully saved the record, so it never moved it to the storage version. It's stored in the cache right now, and if we fill out a description, the hidden field will actually be submitted, not the input field for the file field, and if we create this post, you will see that we get some image data. That means that our cached version of the image is working, and if we go create a new one and we upload the same image and we submit some text, then this will automatically work the first time. That's pretty cool, and we have two of these uploads that have uploaded successfully. On that redisplay, you're probably going to want to show that image so that you know what you previously uploaded, so you can see that, instead of having the input field, so let's go add that to our form to make it more intuitive to work with when the record fails. To do that, all you have to do is say:

<% if post.image.present? %> 
    <%= image_tag post.image_url %> 
<% end %> 

This is going to return you the image url, so this is a method that it creates in order to reference that, and that is going to then be passed into the image tag, and this should also even automatically work with the cached version, because there's a cached version in the object, so it knows that it has an image, it just hasn't permanently stored it, so it's able to take care of those two locations, and pick between those whenever it needs to when you reference it. Let's try and upload an image without a description, so this should fail, and we should see that on the page. So we can do that, and you see the currently active image that I uploaded, and if we just type in a description, that will now get submitted. We are able to display the image you uploaded the first time, so that you know it's there for reference, and then we can check if you want to override that image with the new one, you can go fill out that image field, and that will automatically upload the new image and replace the old one. That is really all you need to do, and you can reference those images like so, and you can check to see if they exist with calling image.present. That's really all you need to do, if you want to go into your show action and display those images, you can paste in the same if the image is present, display the image tag and we can refresh this, and you should see if you use the correct instance variables here, then those images display. We're going to stop there because that is the foundations that you need to know in order to start working with Shrine, and get basic file uploading working, and this includes the cache functionality, which is nice to know. That is, I think a crucial part of uplading files in most applications, you're going to want to make sure that you have that feature in, but what we'll dive into in future episodes on Shrine is how to do background processing, thumbnails, all kinds of those other options including Amazon S3 storage, and we'll go through some more of those plugins with Shrine in order to build a much much much more robust file uploading system. Until then, I will talk to you later. Peace v

Transcript written by Miguel

Discussion