Skip to main content

Remote URL

The remote_url plugin allows you to attach files from a remote location.

plugin :remote_url, max_size: 20*1024*1024

Usage

The plugin will add the #<name>_remote_url writer to your model, which downloads the remote file and uploads it to temporary storage.

photo.image_remote_url = "http://example.com/cool-image.png"
photo.image.mime_type         #=> "image/png"
photo.image.size              #=> 43423
photo.image.original_filename #=> "cool-image.png"

If you're using Shrine::Attacher directly, you can use Attacher#assign_remote_url:

attacher.assign_remote_url("http://example.com/cool-image.png")
attacher.file.mime_type         #=> "image/png"
attacher.file.size              #=> 43423
attacher.file.original_filename #=> "cool-image.png"

Downloader

By default, the file will be downloaded using Down.download from the Down gem. This will use the Down::NetHttp backend by default, which is a wrapper around open-uri.

You can pass options to the downloader via the :downloader option:

attacher.assign_remote_url url, downloader: {
  headers: { "Authorization" => "Basic ..." },
  read_timeout: 30, open_timeout: 30,
  max_redirects: 5,
  # ...
}

You can also change the downloader:

# Gemfile
gem "http"
require "down/http"

plugin :remote_url, downloader: -> (url, **options) {
  Down::Http.download(url, **options) do |client|
    client.follow(max_hops: 2).timeout(connect: 2, read: 2)
  end
}

Any Down::NotFound and Down::TooLarge exceptions will be rescued and converted into validation errors. If you want to convert any other exceptions into validation errors, you can raise them as Shrine::Plugins::RemoteUrl::DownloadError:

plugin :remote_url, downloader: -> (url, **options) {
  begin
    RestClient.get(url)
  rescue RestClient::ExceptionWithResponse => error
    raise Shrine::Plugins::RemoteUrl::DownloadError, "remote file not found"
  end
}

Calling downloader

You can call the downloader directly with Shrine.remote_url:

# or YourUploader.remote_url(...)
file = Shrine.remote_url("https://example.com/image.jpg")
file #=> #<Tempfile:...>

You can pass additional options as well:

# or YourUploader.remote_url(...)
Shrine.remote_url("https://example.com/image.jpg", headers: { "Cookie" => "..." })

Uploader options

Any additional options passed to Attacher#assign_remote_url will be forwarded to Attacher#assign (and Shrine#upload):

attacher.assign_remote_url(url, metadata: { "mime_type" => "text/plain" })

Maximum size

It's a good practice to limit the maximum filesize of the remote file:

plugin :remote_url, max_size: 20*1024*1024 # 20 MB

Now if a file that is bigger than 20MB is assigned, download will be terminated as soon as it gets the "Content-Length" header, or the size of currently downloaded content surpasses the maximum size. However, if for whatever reason you don't want to limit the maximum file size, you can set :max_size to nil:

plugin :remote_url, max_size: nil

Errors

If download errors, the error is rescued and a validation error is added equal to the error message. You can change the default error message:

plugin :remote_url, error_message: "download failed"
plugin :remote_url, error_message: -> (url, error) { I18n.t("errors.download_failed") }

Background

If you want the file to be downloaded from the URL in the background, you can use the shrine-url storage which allows you to assign a custom URL as cached file ID, and pair that with the backgrounding plugin.

File extension

When attaching from a remote URL, the uploaded file location will inherit the extension from the URL. However, some URLs might not have an extension. To handle this case, you can use the infer_extension plugin to infer the extension from the MIME type.

plugin :infer_extension

Instrumentation

If the instrumentation plugin has been loaded, the remote_url plugin adds instrumentation around remote URL downloading.

# instrumentation plugin needs to be loaded *before* remote_url
plugin :instrumentation
plugin :remote_url

Downloading remote URLs will trigger a remote_url.shrine event with the following payload:

KeyDescription
:remote_urlThe remote URL string
:download_optionsAny download options passed in
:uploaderThe uploader class that sent the event

A default log subscriber is added as well which logs these events:

Remote URL (1550ms) – {:remote_url=>"https://example.com/image.jpg",:download_options=>{},:uploader=>Shrine}

You can also use your own log subscriber:

plugin :remote_url, log_subscriber: -> (event) {
  Shrine.logger.info JSON.generate(name: event.name, duration: event.duration, **event.payload)
}
{"name":"remote_url","duration":5,"remote_url":"https://example.com/image.jpg","download_options":{},"uploader":"Shrine"}

Or disable logging altogether:

plugin :remote_url, log_subscriber: nil