Easily add file uploads to any model (including single or multiple files) using Rails 5.2's new ActiveStorage feature
What's up guys? This episode we're diving into Active Storage in Rails 5.2, and this feature is very welcome because file uploading is something that we had to rely on third party gems for, but it's such a common feature to upload files to your rails app, that it is now finally baked into rails core, so we're going to be talking about that and if you've ever used paperclip or carrierwave or Shrine, it's all very similar and you don't have any real reason to upgrade if you're happy with those, but if you're building a new application, and you want to use this, go for it, it is really really well integrated into rails but it's still got a bit of rough edges and there will be some thing that will certainly improve in the next few releases I am sure, so let's dive in to setting this up and taking a look at how it all works, so basically, when you create a rails 5.2 application, you can run
rails active_storage:install to create the migration to add active storages two tables to your application, if you upgrade from rails 5.1 or 4 or whatever to 5.2, you can run this to get those migrations as well, so if you run this command, if you've already got these migration, it's not going to do anything which I do, and that migration looks like this, it's going to create two tables, one called blobs, one called attachments, and the blobs are going to store all the metadata around the file that you uploaded like the file name, file size, content type, all that good stuff, and then the attachments is the join table between your models, whatever they might be and the uploads that you created, so that's going to be as simple as it is, they keep track of that, and the reason this is great is because it means that you don't have to create migrations whenever you want to add a new upload to your models, so you might want to add avatars, or you know, uploads to a post or attachments or images or any of that stuff, you don't have to make any more migrations or anything like that, no more database changes when you want to add new file uploads and that is really nifty, so this is something that I think is pretty well designed and really makes it handy to go add file uploads, so we can go to a model like user and simply say it has one attached avatar, and you are good to go, that is going to set up the methods and the associations so that when you upload an avatar on a user, it will go ahead and create the attachment record and the blob records in the database, and that's kind of awesome. Let's take a look at an example of what you have to do to set this up going forward.
First off, you have to go to config/environments/development and production and look for the
config.active_storage.service = :local
if you created a new rails 5.2 app or add this line if you upgraded to rails 5.2, and you're going to set this to the key in your config/storage.yaml file. This is where you will have your defined locations of file uploads, so I've got a few examples in here, by default you only get tested local, because they don't know your credentials for AWS, they don't know if you even want to use AWS s3 to upload files to, so the defaults are of course to upload to your current machine's file system wherever rails is living. That is pretty straightforward, and that's good in development and test, but in production, you probably want to go to s3, Digital Ocean spaces, Google Cloud Storage, Azure or something else and these are the main options that you have right now. You have s3, you can use Digital Ocean spaces because it's fully compatible with s3, you can use Google Cloud Storage or Microsoft Azure and you also have a sweet mirror service for redundancy, so you can have your files uploaded to two or three of those services at the same time and that will take care of any mirroring that you might want to do, so I would definitely encourage that if you want redundancy, and you just basically set up two of these options and then define them as mirrors down below in the mirror service and you would use mirrror in your config's environment's production file as your service. What I have set up here is one for Digital Ocean where we can use Digital Ocean Spaces, I have my bucket in NYC3 and we can define in our credentials that yaml.encrypted file, the keys for that, so you can run rails credentials edit, which I will link to in the notes below because if you haven't seen that episode, the new credentials file replaces your secrets.yaml, you can use your secrets.yaml if you're upgrading from an old application, this is a nice way to encrypt your keys and still keep them inside of your Git repo. So we'll be using Digital Ocean in production, but I'm going to be using the local disk for development and one option here that I want to point out is host is sometimes required in local because if you're ever running on a different port, it isn't smart enough yet to notice that you're using a different port, so sometimes if you're using, say foreman, you would have to define your host here, so that it links to localhost:5000 instead of localhost:3000, which is the default. Keep that in mind, you may or may not want to add the host option in here for that because for whatever reason, it doesn't know and I feel like that's probably something that will get fixed in a future version, but right now, you might have to manually get that fixed, otherwise we'll get 404's in development.
That's all you have to do to set up Active Storage and configure it, and then you can go to your models, and define your attachments.
Here I've got a Post class which is just simply title and body, and there's no other attachment fields on it, and we can simply say
class Post < ApplicationRecord has_one_attached :header_image has_many_attached :uploads end
To accept these in your controller, you'll go to
... def post_params params.require(:post).permit(:title, :body, :header_image) end
It's like any other attribute, it's going to accept the uploaded file like so, but with your uploads it actually is going to be an array and so in those
has_many you can just simply say
... def post_params params.require(:post).permit(:title, :body, :header_image, uploads: ) end
And then we can go open up our form to add that to it, so let's create one at the top for our header image,
<div class="form-group"> <%= form.label :header_image %> <%= form.file_field :header_image, class: "form-control" %> </div> <div class="form-group"> <%= form.label :uploads %> <%= form.file_field :uploads, multiple: true class: "form-control" %> </div>
Now, I've got a post created already and we can go edit the header image by uploading some sort of picture here, and then we can go to uploads and we can maybe grab, let's just grab a PDF, an image and an SVG, and add those as attachments. We'll go ahead and upload those, we'll see that our post was successfully updated, and if we go to our logs, scroll down to the bottom, we'll see that this was uploaded, we get a patch and our uploads actually are doing some interesting things, so it's deleting the existing attachments from there and then it's going to dis storage and uploading files we'll see it creates Active Storage blobs with the metadata, image.jpeg and all that good stuff, then it goes down and updates our post after it has created our attachments that are the join records between the two, so it's creating all of these associations for us and our attachments you can see, the record type is for post, uploads, record id, so it's doing a polymorphic association here, and it's saying: Well this is for the uploads attachment type with the name field and then the association is for the record field. It's kind of cool how that works, you can see that it is running all of that when we do our update and then at the very end it's end, it's enqueued several analyzed jobs for each one of those files that we did, so it's doing some analysis on those, automatically integrating with ActiveStorage by the way, and so you're going to want to run Sidekiq if you're using that to do some of this in the background, you can also just leave that alone and it will run in your regular rails process immediately as it gets it. So this is all nicely integrated with rails, so you can see that it's using ActiveJob and all of that, but we need a way to display these images. One cool thing about this, is that if you go to the post show page, all you have to do to display an image is say
<%= image_tag @post.header_image %> you don't have to do the .url or thumb like some of the file uploading tools do, you actually just say: Image tag, and you pass in the image object and you're good to go. This will display the image, it's kind of huge right now, so maybe it would be nice to resize it, you can go to your Gemfile and add the minimagick gem to your gem file, and that will allow you to resize images, so now if we go back to posts show after you've installed mini_magick, and you've installed Image Magick on your laptop or your server, that will be required mini_magick uses Image Magick as a separate command to resize images, so keep that in mind. Then you can say
<%= image_tag @post.header_image.variant(resize: "400x400") %>
Now we get a 400 by 400 image, you can use Image Magick's options to do other types of cropping or maybe you want to resize to fit, resize to fill or whatever you want, you can use all of those options from mini_magick inside this variant method, and so the variant method only applies to images though, and that is something that we will notice if we go down here and create a div for all of those other uploads, so our post has uploads, and if we were to loop through each of these uploads and do the same thing with the image tag, we're going to get an error here because we've uploaded a few thing that are not actually images, so when you get an invariable error on this upload, if we take a look at the upload itself, you can see that we don't really know which one it is, but if you say
upload.blob, you'll be able to figure out that, this is our PDF that we uploaded, and it is not resizable, because it's not an image, you can't resize a PDF, but there is built in functionality for previewers, which is really cool. So here is what we can do, and say
<div> <%= @post.uploads.each do |upload| %> <% if upload.variable? %> <%= image_tag upload.variant(resize: "400x400") %> <% elsif upload.previewable? %> <%= image_tag upload.preview(resize: "400x400") %> <% end %> <% end %> </div>
What this is going to do is create a preview image of our PDF, it will use a thing called mu tools, or mu tool, if you want to install that, you can go to your terminal and run
brew install mupdf-tools ffmpeg
You can install these two tools on your machine and then as long as they're available, you'll be able to use them to resize and preview things, so if we do this, we will now get our Prawn PDF example right here and it's displaying this cropped to maximum of 400x400, and you know, it's a little narrower, since we're not using, you know, resize to fill or anything like that in mini_magick, but it does work, you can click on these, or well, we can create these and make them clickable so we can have links to these images as well, we can do the image like so, and then we can link to upload here, and if we do that, we will be able to click on this image and then open it up, and you'll see the route for this is /rails/active_storage/disk and then our big key for this file and you get a little bit of information about it at the end, so this is pretty cool, and you can also change things like the disposition, so right now it's in line, which is going to display it in the browser, but you can change that disposition to I believe attachment, which will cause your browser to download it instead of displaying it, so when we link our PDF's here, you might want to force that PDF as downloaded instead, but we're now going to use, instead of just upload directly, we're going to use a new helper that you can use,
rails_blob_path, this url helper is going to point to the active storage blob, you give it a blob and then you can say:
<%= link_to image_tag(upload.preview(resize: "400x400")), rails_blob_path(upload, disposition: :attachment) %>
And then that url now will download the PDF directly instead, so that's kind of nice for any of those where you may want to force it to be downloaded, you can do that, you don't have to do that with PDF's, that's just a good example of how that might work. Now, otherwise, we also want to handle any cases where it's not variable and it's not previewable, and we just want to do that, so maybe in this case, that's one of the ones we want to use the rails blob path and we just want to link simply to the upload itself, and so in this case we might have our upload.filename and we just want to link our gorails.svg file here because it turns out that SVGs are not variable and they're not previewable and so that makes them kind of a weird case here because technically you can resize them, but you can't resize them with mini_magick because they're vectors, they can be scaled automatically and they don't need to be resized with a tool like mini_magick, and so they kind of fit in this weird case, but Active Storage still has another helper for us, you can actually check if an upload is an image and then in that case, we know that it's not variable, so we can have an image tag for the upload and maybe in this case we set the image tags with in HTML to 400 instead of setting that, and so we could do that and go back to our browser, now we'll see the SVG is displayed here appropriately, but it's a little bit different than you might have expected because SVG's are kind of an odd type of image, but we got the width of 400 and we'll still match that and everything will still work as you might have expected.
So you really will have to keep in mind how you want to display and render those uploads, but that is up to you, you have quite a bit of flexibility with that, and I believe there are also some more helper methods like this to figure out what types those uploads might be so that you can add your own views for each one of those options. That works really well and does quite a good job of all of that.
One last thing I want to mention here, is that with the previewers, you have just PDF and video formats right now, we might do a future episode building a previewer for something like, I don't know, a slideshow like you can maybe take and upload a keynote slide, deck and then maybe we'll pull out the first slide as an image, if you're interested in that, let me know in the comments below, I'm sure someone is going to be working on adding more of those file types in there to be really cool to see as we go forward, and last but not least is you want to be able to delete these images, and one of the things with that is that you have a method, so if we grab this post, you have a method on your association, so a header image can call purge or purge later, purge later is generally the better option to go and that will go a background job to run and delete it later, but if you need it to run immediately, you can just call purge and that will delete that file, no the same thing goes for the uploads, if you want to delete all of them, you can call purge on them and then it will delete all of them, or you could say like your first upload and purge that one, so if we do that we can go back to our browser adn you'll see our Prawn PDF is gone and that just deleted a single one, but if we did
post.uploads.purge you can see it will delete all of them and all of our files are gone, so keep that in mind, that is how you delete your files from Active Storage, you can basically pass in the Active Storage blob ID so when we're in the browser and you're doing one of these uploads, you can actually print out the upload so for example post header image id 18 that would be the thing we could go purge, we can then look up
ActiveStorage::Attachment.find(18), the header image and you can call purge on that to delete it, so if you ever want to go and delete individual ones in the UI, you can actually just submit it over the IDs for the Active Storage object and look them up through Active Storage and have them deleted like so instead of going through your model, that will work pretty well.
As far as I know, some of the other gems offer the ability for you to flag attachments as delete and so you could submit your posts and then it would delete those attachments, but as far as I understand, ActiveStorage doesn't have that ability right now, maybe it will be something added as a future feature but for now it's easy enough for you to create a new controller for attachments or uploads and then look up the Active Storage attachment by id, then call
Transcript written by Miguel