Skip to main content

24 File Uploading with Carrierwave

Episode 11 · July 7, 2014

Using Rails to upload files manually and how you can do it even cleaner using Carrierwave

Gems File Uploading


Transcripts

So file uploading is the next feature we want to add to our application, and we're going to talk about how to do this in pure rails, as well as using the carrierwave gem to simplify things considerably. So, what do we want to upload files for? Well, our books like "Mastery" could use a bit of sprucing up, they could use an image for the book cover, and it would be nice if we could click on "Edit" and see a file field and be able to upload an image there. So that's what we're going to do. This is pretty straightforward, however. When we do this, we're going to need to do a fair amount of work because we have to understand how rails works internally a little bit, so that we can place the files in the proper directory so that they're available for the browser. So if we jump into our terminal, and we take a look at the public folder, this is pretty special. So the public folder** is static files that are served up by your web server and they hit before rails does. So if you type "404.html", it will bring up the 404 page just like you see here, but this is not being served up through rails. It sees 404.html inside this folder, and it immediately serves it up, so there's no processing going on, there's no database lookups or hitting your rails routes or any of those things. It sees the file name and it serves it up. If this didn't exist, it would go through rails and look for a route and go though the regular process, but it doesn't because this url matches exactly that. So we're going to take advantage of this knowledge, and we're going to put it here, a folder for the book in the book covers. So we're going to have to design how that works so that we can properly implement this and handle it. In our terminal, we can go into the same public directory and list out the files. And if we think about it, this is the perfect place to put our uploaded images. We can store them here, and they'll be served up without ever processing through rails, which will be very quick compared to going through rails, since they're static files. So in here, we want to separate everything out very cleanly because things can change in the future, and we want to be ready for that. So for now, we're just uploading images for books, but what if we want to add avatars for users later on? Well, that means we should separate those out into folders at the hightest level. So if we make a directory called books, here we can store all of the images and file uploads for books. If we make images and avatars for users, then we can have a users one, and separate those two out cleanly.

Let's talk about what else could change, what if our books have multiple images, what if we have an image for the book cover? For the back of the book? What if we have an image for the author? All of these mean that we could have more folders inside of here for every single book. So at the next level, we want to add a folder for a dynamic folder name that is the book database id, or the database slug. We're going to use the database id because the slugs can change, and we're just going to stick with the id's to keep it simple. So book number two is "Mastery", and we're going to hard-code an image in here and test it all out. So if we create book number two here and we dive into that, then we can talk about what we just mentioned where what if the book has a cover, a back cover or an author image? So in here, we want to separate those out. We don't have those yet, but we want to think about it in case we ever do, so here we're going to put the cover folder, and then inside the cover folder, you're going to upload the file directly into this folder. I'm going to simulate thay by just copying an image in here and saving it. So we could rename the images, but it really doesn't benefit us that much. If we keep the original filename, we can save this mastery_cover.jpg into the database and then our book knows that it does have a cover. So if our field is empty in our database record, then it knows that there is no cover, and we can use regular rails ActiveRecord code to skip the image, and if it does have one, it can look and point the image tag to this folder. So this is how we want to structure the folders in the image uploading at the very lowest level on your file system. So if we dive into our code in the show view, we can do something like this, which is still somewhat hardcoded. So we can say there's an image tag for /books/#{@book.id}/cover/Mastery_Cover.jpg, and if we open this up in our browser, the image loads. So this knows to automatically look for the image where we stored it, and if your paste the image url into your browser, you can see that its books/2/cover/Mastery_Cover.jpg, which is exactly the same folder and file name as we just created in our terminal. Now that we've figured out where we want to save our images on disc, we can go to the edit form and the new form and begin adding the file upload field as we had planned in the very beginning. So we need to add the field, we need to have it save the file, and we also need to save the file name into the database record. So let's start with the form

app/views/books/_form.html.erb

//...
<div class="form-group">
    <%= f.label :cover %> 
    <%= f.file_field :cover %>
</div>

Now if we refresh the page, we have a cover attribute that we can upload. So if we upload the mastery cover again, and update book, nothing changed, nothing crashed, but it didn't actually do anything and the reason why is because if we go to our rails logs, we can come back to the PATCH method that we just saw, and we can see unpermitted parameters cover. So the cover file is being uploaded, and as you can see here, it is an action dispatch uploaded file instance, it has a temp file assigned to it, and we can see the file name and the content type and everything about it. and that means that our file is being uploaded correctly, however, we're just ignoring it when we recieve it. So we need to go into our books controller to actually allow it to happen. So if you jump down at the bottom, you can add cover into the book params method as a permitted attribute. So this will now allow the cover image to be submitted, and it will try to assign it to the book. However on the book, we don't actually have anything but FriendlyID on there, we've never added the method for this cover, and we don't even have an attribute for the cover file name that we need to save. So let's start there. Let's go into our terminal and generate a migration called

rails g migration AddCoverFilenameToBooks cover_filename

rake db:migrate

We finally have everything we need to actually upload and process this image, and that's what we're going to do now. so if we add an attr_accessor :cover this allows the controller to assign the image we uploaded to the cover attribute on this book, and then we can go doing the actual saving into the file system where we want. So if we add an after_save callback and we'll call it save_cover_image, this allows us to take that temporary file that we uploaded and really save it into the public directory like we talked about. So we only want to do this if there's a cover image. So if this is nil, then we won't try to save the image again. So then we can add

app/models/book.rb

class Book < ActiveRecord::Base 
    extend FriendlyId 
    friendly_id :name, use: :slugged 

    attr_accessor :cover 

    after_save :save_cover_image, if: :cover 

    def save_cover_image 
        filename = cover.original_filename  
        folder = "public/books/#{id}/cover"

        FileUtils::mkdir_p folder  

        f = File.open File.join(foler, filename), "wb"
        f.write cover.read()
        f.close

        self.cover = nil 
        update cover_filename: filename 
    end
end

So what this does very simply is it creates the folder, it writes the file to it, and then it updates the book record with the filename. So we can add a new file, a different file name and it will always know which one to point to, and self.cover = nil is very important, because if you don't have it, it will go into an infinite loop and continue trying to save this cover image over and over and over again. Now that we have the file uploading working, we can go into our show action, and we can remove the hardcoded file name that we had before, and replace it with Book.cover_filename if Book.cover_filename?, so this will only display the image if the book has had an image uploaded for it. So now we can go into our public folder again, and let's remove the images that we have, and the folder that we created before, and then we can go to our application, we can go into "Mastery", there is no image being displayed, and if we edit and we upload Mastery's cover image, now it displays, and now if we go back and we go to "How to win friends and influence people" and do the same thing, we can see that it also displays properly. If we go into one of the books that I haven't played with, you can see that there's no image here. Well this code isn't terribly complex, it starts to get pretty nasty if we start adding in image editing like cropping and resizing, and even doing work like uploading these images to Amazon s3 or Rackspace cloud files makes this a whole heck of a lot more complex. Not to mention, if you wanted to add an author image here, you pretty much have to duplicate everything we just wrote. So what we're going to talk about next is how you can use carrierwave to replace this. But now that you have a good understanding of why this is important and how carrierwave works at the most basic level, you'll be able to actually use carrierwave pretty extensively, and it won't feel like random magic that someone put together for you.

Now that we've design our own mechanism for uploading and storing files on our server, let's take a look at how carrierwave does it. Now the one thing that I want to point out here is that carrierwave has a concept of an uploader. And an uploader is a ruby class that is defined, and it inherits from carrierwave's internal helpers. So it has a class that you basically you mount on your ActiveRecord model that says: Ok, any interaction with this cover attribute will be through carrierwave, so it's going to help you store the file weather that's on your local server or on Amazon s3 or Rackspace cloud, and it's also going to handle all the image processing that happens, and you can also do things such as configuring the storage directory where your files are saved. So this is what the uploader is designed for, it's to encapsulate all of the logic that happens inside of your application when an image is uploaded, and the reason why they do this is because like we talked about earlier, if you were to have an image for the book cover, as well as an image for the author, you might want to process those separately. So one of them might need to be a certain size, and the other one might need to be a different size. Now you can create two uploaders and separate all of that code out very cleanly in between the two. So to transfer over our custom file uploading system to carrierwave, we're going to first install the carrierwave gem, and that goes in our Gemfile, at the bottom, and we can run bundle install, and we can restart our rails server, and then we can run the rails generate uploader to generate a carrierwave uploader. I'm going to call this one cover so that it generates the cover uploader, and this will be what we use to handle cropping or whatever else we want to do with the cover images. So now that this is generated, let's take a look at what it does.

In here, we can see at the top there's a couple comments for plugins to carrierwave that you can install that allow you to do ImageMagick to do image cropping and scaling. So if you'd like, you can follow the carrierwave README and learn about how to install ImageMagick and enable these features. The next one is storage file which basically tells it: Save to the storage directory here that we've chosen. So this is very similar to how we layed out the folder structure for our book covers, what they do is they have the class name of the book first, then they have cover, which is what the uploader is mounted as, and then the model id. So that is how the file storage saves to a certain location, and then at the bottom you can override filename's and you can also enable a whitelist of extensions so that only jpgs or GIFs or pngs are allowed. And the option underneath storage file is called storage fog and fog is the rubygem that allows you to interact with remote file systems basically. So if you're going to use Amazon s3, Rackspace, cloudfiles, or you want to do something different, you can use fog to interact with those remote systems, and then when carrierwave recieves a file that's uploaded, it will go and save them remotely. So this is how carrierwave defines all of it's customizations for an upload, and you just simply configure it in here. There are only two more things we need to do to finish installing carrierwave, and that is to rename our column on our books table, and change cover file name to cover. And then we need to tell the books model that the cover database column is where we want to store the uploaded files.

rails g migration RenameCoverFilename

db/migrate/rename_cover_filename.rb

def change 
    rename_column :books, cover_filename, cover 
end 

rake db:migrate

app/models/book.rb

class Book < ActiveRecord::Base
    extend FriendlyId 
    friendly_id :name, use: :slugged

    mount_uploader :cover, CoverUploader 
end 

Anytime that a file is assigned to a cover attribute on a book, carrierwave will step in and handle it, and do everything that you've defined in the cover uploader. So with that, we can take a look at our rails application, it's uninitialized constant because we need to restart our rails application, and if we restart it now, everything is set up properly after we've installed carrierwave, and now we can come into our book, and we'll see that the image tag that we were using before no longer works, because we don't have the cover_filename attribute.

With carrierwave, the way to access the images, it's very simple and much cleaner than what we've wrote before. So here's what we've wrote before, and here's what we can do with carrierwave. So you can say

<%= image_tag @Book.cover.url %>

That's as simple as it is. They take the cover attribute and add some methods onto it to retrieve the URL for it, and we had to build the url ourselves. Now we could have spent a whole bunch of time moving this in to make this something compatible with carrierwave, but there isn't really much point in doing that when you can just use carrierwave.

The new image tag is working, but because we've uploaded the images to a different folder name, it's not available. So we're going to add the same thing here, and say if Book.cover? and make it so that the image does not display if carrierwave doesn't see a cover. This is a little bit better, just because it's very clear if there's a book cover, so our code is really readable this way with carriere wave, and it makes a big difference as your application gets bigger. So we should be able to now go into "Edit" and we'll reupload the "Mastery" cover, and there we go. If we want to compare this to before, we can open the image in a new tab and take a look at the url, which is just about the same. It's now in an uploads/book/cover and then the database id, so ours was the database id first. It doesn't really matter either way, the way that I designed it, where you have the database id first means that all of the images for a single record are in the same folder which can be pretty convenient if you're going to do some manual work on like messing with images on the server. That is carrierwave, and I hope you learned quite a bit about file uploading, there's a whole ton to it, and I highly recommend checking out fog and playing with Amazon s3, because it's free for a year, and it's worth checking out.

Transcript written by Miguel

Discussion