How to download image files in Ruby

March 25, 2019
Written by
Phil Nash
Twilion

MSjyG8qNtcrbONBpWKztYRJqSpr4R2M6K83KXW4dj05n5p1Q2Ez4kQyLxiX69xnjo2gBNd-KleVK_tnzFhPbqCK70HVdlI-BR9H8Dk8KRL8RcwlpsYT5uysgm5ado7pxcASFHP4

Have you ever needed to download and save an image in your Ruby application? Read on to find out how.

Plain old Ruby

The most popular way to download a file without any dependencies is to use the standard library open-uri.

Kernel#open is a method that you can use to open files, streams, or processes to read to or write from. For example, you can open a file and read its contents with the following code:

open("./test.txt") do |file|
  puts file.read
end

open-uri extends Kernel#open so that it can open URIs as if they were files. We can use this to download an image and then save it as a file.

To do so, we first require open-uri then use the open method to access an image URL. We can then open up a file and write the contents of the image to the file. Open up IRB and try the following:

require "open-uri"

open("https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/test.original.jpg") do |image|
  File.open("./test.jpg", "wb") do |file|
    file.write(image.read)
  end
end

In the directory in which you opened IRB you will now find the image you downloaded.

This is a success, but this was a straightforward example. In practice you would want to handle potential errors, such as a 404 error for a missing image. Plus, there's a bunch of other potential issues with using open-uri.

Problems with open-uri

The thing is, using open-uri like this is not ideal. First up, the above code is not very memory efficient, it loads the entire image into memory and then writes to disk. It also turns out that open-uri has some other quirks. Janko Marohnić discovered a bunch of these issues while working on Shrine. Notably, open-uri:

To solve all of this, Janko created the Down gem. It allows you to avoid these issues to safely and efficiently download files.

Improving downloads with the Down gem

Let's download the same image using Down. Start by installing the gem:

gem install down

We can download the image to a Tempfile using Down.download:

require "down"

tempfile = Down.download("https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/test.original.jpg")

If you want to save this file to the file system, Down has an option for that. Passing a directory as a :destination option will save the file to that directory.

require "down"

Down.download("https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/test.original.jpg", destination: "./")

This will give the downloaded file a random name generated by Tempfile. If you want to keep the name of the file from the URL you need to do a bit more work. In this case we can download the file to a Tempfile and then move it to a permanent location on our drive.

For this we'll use FileUtils#mv which takes two arguments, the file name and the destination we want to move it to. To get the original name of the file, the Tempfile object that Down returns has an original_filename method we can use.

require "down"
require "fileutils"

tempfile = Down.download("https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/test.original.jpg")
FileUtils.mv(tempfile.path, "./#{tempfile.original_filename}")

Now we've downloaded the file safely and efficiently and stored it permanently on our hard drive.

Advanced usage

Down takes a bunch of other options too, to control the download experience. By default it only allows 2 redirects, but you can control that with the max_redirects options. You can also limit the size of the file with the max_size options. This stops attacker tying up your server with giant image downloads. If you wanted to download a file that was at most 5MB in size with at most 5 redirects, you would use:

Down.download(
  "https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/test.original.jpg",
  max_redirects: 5,
  max_size: 5 * 1024 * 1024
)

There are more options in the documentation, including how to stream files down and change the back-end of the gem from open-uri and Net::HTTP to HTTP.rb or Wget.

Next steps

In this post we've seen how to download images using both open-uri and Down. Down is much safer as it saves us from infinite redirect loops and remote code injection, and it is more user friendly as it always returns a Tempfile and lets us easily restrict file size.

You could use this to download media from Twilio MMS or WhatsApp messages or hook this up with Active Storage in Rails to make more options for users uploading images.

Have you used alternative methods for downloading images in Ruby? Let me know how in the comments below or on Twitter at @philnash.