Skip to main content

Upload Endpoint

The upload_endpoint plugin provides a Rack endpoint which accepts file uploads and forwards them to specified storage. On the client side it's recommended to use Uppy for asynchronous uploads.

plugin :upload_endpoint

Setup

The plugin adds a Shrine.upload_endpoint method which, given a storage identifier, returns a Rack application that accepts multipart POST requests, and uploads received files to the specified storage. You can run this Rack application inside your app:

# config/routes.rb (Rails)
Rails.application.routes.draw do
  # ...
  mount ImageUploader.upload_endpoint(:cache) => "/images/upload"
end

Asynchronous upload is typically meant to replace the caching phase in the default synchronous workflow, so we want the uploads to go to temporary (:cache) storage.

The above will create a POST /images/upload endpoint, which uploads the file received in the file param using ImageUploader, and returns a JSON representation of the uploaded file.

# POST /images/upload
{
  "id": "43kewit94.jpg",
  "storage": "cache",
  "metadata": {
    "size": 384393,
    "filename": "nature.jpg",
    "mime_type": "image/jpeg"
  }
}

This JSON string can now be assigned to an attachment attribute instead of a raw file. In a form it can be written to a hidden attachment field, and then it can be assigned as the attachment.

Calling from a controller

If you want to run additional code around the upload (such as authentication), mounting the upload endpoint in your router might be limiting. You can instead create a custom controller action and handle upload requests there using Shrine.upload_response:

# config/routes.rb (Rails)
Rails.application.routes.draw do
  # ...
  post "/images/upload", to: "uploads#image"
end
# app/controllers/uploads_controller.rb (Rails)
class UploadsController < ApplicationController
  def image
    # ... we can perform authentication here ...

    set_rack_response ImageUploader.upload_response(:cache, request.env)
  end

  private

  def set_rack_response((status, headers, body))
    self.status = status
    self.headers.merge!(headers)
    self.response_body = body
  end
end

Limiting filesize

It's good practice to limit the accepted filesize of uploaded files. You can do that with the :max_size option:

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

If the uploaded file is larger than the specified value, a 413 Payload Too Large response will be returned.

Uploader options

You can pass additional uploader options via :upload_context:

plugin :upload_endpoint, upload_context: -> (request) do
  { location: "my-location" }
end

Note that the uploader will not receive :record and :name values, as the upload happens independently of a database record.

Upload

You can also customize the upload itself via the :upload option:

plugin :upload_endpoint, upload: -> (io, options, request) do
  Shrine.upload(io, :cache, **options)
end

URL

You can have the endpoint include the uploaded file URL in the response body by specifying the :url option:

plugin :upload_endpoint, url: true
# or
plugin :upload_endpoint, url: { public: true }
# or
plugin :upload_endpoint, url: -> (uploaded_file, request) {
  uploaded_file.url(**options)
}

In this case the response body will be:

{
  "data": { "id": "...", "storage": "...", "metadata": {...} },
  "url": "https://example.com/path/to/file"
}

Response

The response returned by the endpoint can be customized via the :rack_response option:

plugin :upload_endpoint, rack_response: -> (uploaded_file, request) do
  body = { data: uploaded_file.data, url: uploaded_file.url }.to_json
  [201, { "Content-Type" => "application/json" }, [body]]
end

Ad-hoc options

You can override any of the options above when creating the endpoint/response:

Shrine.upload_endpoint(:cache, max_size: 20*1024*1024)
# or
Shrine.upload_response(:cache, request.env, max_size: 20*1024*1024)

Checksum

If you want the upload endpoint to verify the integrity of the uploaded file, you can include the Content-MD5 header in the request filled with the base64-encoded MD5 hash of the file that was calculated prior to the upload, and the endpoint will automatically use it to verify the uploaded data.

If the checksums don't match, a 460 Checksum Mismatch response is returned.