Skip to main content

32 File Uploads with Refile

Episode 37 · January 3, 2015

Learn how to upload files with the newest file upload library on the block: Refile

Gems File Uploading


Transcripts

This episode we're going to talk about the refile gem, and I'm really really excited about it. I'll post a link to this blog post in the show notes, but Jonas Nicklas, who has worked on carrierwave for a very long time has had a ton of experience supporting the rails community, and handling file uploads. That's not a trivial task. The file uploading stuff in rails is mediocre at best, I'll say. You've probably used paperclip, and paper clip you have to define all of the code in your models, we can pull up an example here. Thoughtbot did a great job on it, but the location of your code is a little bit confusing, and actually a lot of it is very confusing. In rails 4 you have an attached file, and you say: Let's have an avatar, and then let's crop it to medium and thumb, and here's the default url, and then let's validate the content type of it and so on, we have to create a migration, and we have to set multipart => true in our forms, and so on. It works, but you have to add like four columns to each of your models, you have to add validations, and then when it comes to doing things like Amazon S3, well you have to just keep putting more and more code in there in different places, so it ends up being a little bit messy. Now, carrierwave took a different approach, and carrierwave separated that code out into different classes. When you would say you wanted to add an avatar, they create the separate uploaders file, and that class inside of there is what defines like all of the things like the styles, like how to crop the images and so on. There's all these options and methods that you can define, like where to cache it, where to store it, what file types you want to allow and all of that. carrierwave was nice and it did many magic and stuff and you could process things, and it was not bad, but it still left a lot to be desired. Jonas Nicklas started this new gem called refile, and it's pretty awesome. I'm really really excited about it. Installation is really really short actually, you add these two gems, you add your attachment, and this user has a profile image, you add one string to your model, and then you have this attachment field, which is like file upload field, it's a helper for some other functionalities, you of course set up strong params for it, and then you have this magic attachment url that works quite a bit differently than what you might have seen. You notice here that we didn't define how to crop these images, well, that is defined in your views, so when you add in an attachment url, we say we want the profile image for the user, and let's crop it with the fill algorithm, and let's make it 300 by 300.

This happens on the fly with number three here, which is the magical rack application, so you have a rails app that's running, as well as a Sinatra app, and your rails app, when you upload a file, will send the image to the Sinatra app. It will do storage and everything, and then when you want to display an image with the image tag, it will actually stream the image back to you and resize it on the fly, so that's really nifty. One disadvantage of that is that you basically are required to have a CDN in production because all of this is happening on the fly, an that's a heavy tax for your servers, but pretty much every application should use a CDN in production anymore, and they're really cheap or free in some cases like CloudFlare. I'm really excited about this gem, one of the other things that I wanted to show you, and I welcome you to read through all of this, it's very well written and it explains a lot. For example, this shows the attachment field helper, how it automatically adds the encoding of multipart when you have the attachment field, and then it sets the hidden and type file in there so that it automatically handles all those exceptions when there's no file added and everything like that. The coolest part is that it comes with a JavaScript library that you can require with refile in your application.js, and then it adds this direct: true option. What that does, is it says right here, it immediately starts uploading the file as soon as the user selects it. They can fill out the rest of the form, but the JavaScripts will go back and upload it for you in the background. You've got these progress callbacks, and that's what people wanted. You upload files with paperclip or carrierwave in the background, and you had to use jQuery file upload, and it was a huge pain in the butt to do, and often was so unstable that it didn't work half the time. This JavaScript just comes with it, and you can just add these examples in, and for example this one disables the submit button until all the files are uploaded. That's really really nifty. What more could you want? It's only a few lines of code, really really simple. It also supports things like presigning uploads to go straight to Amazon S3, and you can say presigned: true, but you can also do background ones to Amazon S3 as well, so you can do the direct option there too. It's impressive.

Two last things before we dive into implementing this gem. So many people over the years wanted to figure out how they can remove the attached files, and this just comes with remove_profile_image attribute that you can set in your forms, so you can check that box, remove profile image, which is picked up by refile, and automatically gets rid of that attached file for you. That's nifty, and grabbing files by url, SO awesome, all you have to say is remote_profile_image_url and it will automatically fetch the image and then add it to the model. Plus, it follows any redirects too, so that's really cool, if you grab like an image from google or something, and the link redirected, then you'll still get the correct image as opposed to like just a 302 response. Refile, I'm really excited about, really happy with how it's designed, and the code that you implement with it, so we're going to take a look at our movie database that we did previously, and if you remember, I just took these images, and if we inspect this, you'll see that I just had a string column, and grabbed the image links from IMDB, we're going to replace this with refile, and we'll allow the image links like these, but we'll see how that changes, so we'll specify it using that remote_profile_image_url, and then we'll see what happens with the url when refile takes care of it.

Diving right in, we have this film model, and it has a url that is just saved as the image url attribute, and the same can be said about the actor model. We have an actor, we have the image url, and we're going to rename those and just basically change that to the image id that refile is going to use. First thing is first, let's paste in the mini_magick and the refile lines to our gemfile, you want to be careful here, because you need to specify these require options here:

gem 'refile', '~> 0.4.2', require: ["refile/rails", "refile/image_processing"]

If you don't do that, you're going to run into the missing attachment method inside your models, and so that is going to be requirement there. Make sure that you get that, save this file, you can run bundle to install it, and then let's just rails g migration RenameImageFields I'm going to name it that because we're actually going to take care of renaming both those image fields.

db/migrate/~timestamp~_rename_image_fields.rb

class RenameImageFields < ActiveRecord::Migration 
    def change 
        remove_column :actors, :image_url, :string
        remove_column :films, :image_url, :string 
        add_column :actors, :image_id, :string
        add_column :films, :image_id, :string
    end
end

rake db:migrate

Our columns got removed and added. You could rename this, it would keep the data that's currently in there, but that's kind of bad because the current string that are in there wouldn't actually match up to files that were uploaded to refile, so the existing data I wanted to blow away and that's how we're going to do that. Let's restart out rails server. Now, back in our models, we can add the attachment :image to each of these films and the actors, and then we can hop into the film form, and then we can change the text field that we had previously and reference the image now, instead of the image url, and we can remove this and change it to attachment_field. That should replace it with a file upload field now, and we'll check that out in a second in the browser, and then we'll also do this to the actor form here. Let's save that, and then refresh our edit page, and now we have an image upload field here, so we have the same form that we previously did, I just haven't styled it, but now we have a working file upload field. If we actually choose one of these images, and grab Keanu's face, and choose this, the JavaScript isn't added to the page, so this doesn't get uploaded in the background, this is going to happen when we click "Update Actor". Before we do this, we need to update the actors/show.html.erb page, and here we have the image tag for the actor image url, and that's not going to work anymore because we have this attachment url to use instead. We have

<%= image_tag attachment_url(@actor, :image, :fill, 300, 300) %>

We'll just use their example and see what this does. We can go to films/show.html.erb

<%= image_tag attachment_url(@film, :image, :fill, 300, 300) %>

Let's upload this actor, and we didn't get an image, so that's interesting. We have an image source of nothing, what happened was, we forgot to update the actor controller, and we need to change our strong params here to accept the image, and same with the films. We need to go to films_controller.rb, jump down to the bottom, and add in image. Now when we go do this, it removes the image actually, which is very cool. We can choose Keanu, and when we come back, when this page loads again, we get a 300 by 300 cropping of Keanu Reeves, that actually is really really neat. I ran into a bug here when I was trying to display this in rails 4.2.0 rc1. You may need to make sure that you're running a stable version of rails, not the pre release. I had to update rails to make sure that this worked but it did successfully, and we can go to our films now, and if we change this image tag here, and I was setting the style as a height of 150 pixels on the films index page, and we can go change this to the attachment URL, we can do an image tag for that. Now, we can say: instead of style as height that we can set

<%= link_to image_tag(attachment_url(film, :image, :fill, 150, 150)), film %>

These don't have images yet, we can edit them, we can add image for the Matrix, upload the film, the Matrix has it's image because it's square it didn't show up fully, so let's play around with these image tags, so you can actually go here and do anything you want. You can change these to 250 by 250, the image will be larger, you can change it to 50 by 150 and you will get a larger image, and all of that. You can do any of these of these options here, and they will change on the fly because of the way that this Sinatra application works. Here you can see that this file that gets requested in our logs is actually that image that we grabbed. When you run rake routes, you'll see this attachments url here that gets mounted to this refile app. This is your Sinatra application, and it's stored inside of the gem in the refile/app.rb It's the one that responds to these /attachments/store and here is the options that we give it, we told it fill, and we told it the width and the height, and this is the file name, so this is the id that is saved on our model. Let's just run rails console here and check that out. Let's grab the first film, and on the first film, "The Matrix" the image id is this big long string of text, so b873 is the first few characters of that, and you can see b873 is in the url, and then we are looking for the image attachment, so this is the url that the Sinatra app looks at, and then parses, and then returns that cropped image on the fly for you, so that's really nifty. The trouble with this of course is that every single request of this url, while it might be quick in development, you can see that it's very fast, this is going to be done thousands, or hundreds of thousands, or millions of times on your server if you have lots of users looking at images, and that's a lot of work. The CDN comes into play here, so that what happens is a user comes in, requests this page, and then hits the CDN first. The CDN looks at this image and then saves a copy of it, and then it's not loaded on the fly and cropped every single page view, it's loading the cached version of it. This is nifty, and that is a huge feature of this gem that you can just change any of this on the fly and see how it goes. It's very very useful. Now that we have that, let's try adding some other functionality in here. Let's go down to the Removing Attached Files, and we'll copy this checkbox for removing those, and then we'll also do the uploading images by url. I want to have a checkbox here that says the image here, we want to be able to remove it, so let's go into the film form, and then say:

app/views/_form.html.erb

<%= form.check_box :remove_image %> 
<%= form.label :remove_image %> 

films_controller.rb

def film_params 
    params.require(:film).permit(:title, :description, :image, :remove_image)
end 

Now, if we refresh this page, and we got an undefined local variable form because their example doesn't use f like I normally do, so now we have this "Remove Image" and we can display the image here so that we can see what it looks like. Let's just copy this from the films's show, and display the image. Let's do a small version, 100 by 100. You'll be able to see the preview of the image here, and if you check remove the image, you'll update the film, and the image is gone from here as well as the image has gone from here. That's pretty cool, I believe that we had a method to check to see if the film has an image. Let's test that out in our console, and let's say that Film.first, and now, the image id is nil, and we'll say image? It doesn't look like this image? method is provided by refile, but because this is simply a text string, then we can use image_id?, and it will tell us true or false if there's an image id saved to the database. Rather than talking to image?, they didn't create that method, which is fine. They might add it in the future, because I could see it being useful and very simple to add. Instead, now, if you want to check to see if an image exists, look at the image id column and check for it's presence there. Here, of course, we want to display the check box, remove image field if there is one. Let's say:

<% if @film.image_id? %> 
    <%= f.check_box :remove_image %> 
    <%= f.label :remove_image %> 
<% end %> 

Back in our browser, we can see that there is currently no image, and we can also wrap, if you see here, this field, we have the image with the empty source, and it's actually taking up some space here. If we delete it, it goes back to normal. We also want to encompass the image tag here inside that if statement. That way, we can display the image if there is one, as well as the "remove image" check box, and we can do the same thing here for the actor form, and we could paste that in, and we could replace film with actor, and that's that.

Now, our actors can have the same deal, but now, if we go to the actors page, we also are missing the actors index does not have the correct image tag, so we need to add the attachment url for the actor, the image, and we can set :fill, 150, 150 If we refresh that, we get back to 150 by 150 pixel image there.

That's all for this episode, I hope you enjoyed learning about refile and I definitely recommend checking it out, I don't know of any good examples of migrating you data from carrierwave of paperclip to refile, but if you find any links to that, definitely share them, I'd love to see that, and maybe we can do an episode on that in the future as well.

Discussion