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
:
- leaves you vulnerable to remote code execution
- sometimes returns a
Tempfile
other timesStringIO
- doesn't let you limit redirects or file size
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.
Top comments (0)