Skip to content

Adding Direct App Uploads

Janko Marohnić edited this page Nov 18, 2019 · 21 revisions

This walkthrough shows how to add asynchronous uploads to AWS S3 in a Roda & Sequel app, though the instructions are equally applicable to a Rails & Active Record app. The flow will go like this:

  1. User selects file(s)
  2. Files are uploaded asynchronously to an upload endpoint on your app
  3. Uploaded file JSON data is written to a hidden field
  4. Form is submitted instantaneously as it only has to submit the JSON data
  5. JSON data is assigned to the Shrine attachment attribute (instead of the raw file)


Add Shrine to the Gemfile:

# Gemfile

gem "shrine", "~> 2.11"


Create an initializer that will be loaded when your app boots, where you configure your storage and load initial plugins.

# config/shrine.rb

require "shrine"
require "shrine/storage/file_system"

Shrine.storages = {
  cache:"public", prefix: "uploads/cache"),
  store:"public", prefix: "uploads"),

Shrine.plugin :sequel # load integration for the Sequel ORM
Shrine.plugin :cached_attachment_data # for forms
Shrine.plugin :restore_cached_data # refresh metadata when attaching the cached file


Create an uploader for the types of files you'll be uploading:

# uploaders/image_uploader.rb

class ImageUploader < Shrine

Now add an attachment attribute to your model:

# models/article.rb

class Article < Sequel::Model

You'll also need to add the <attachment>_data text or JSON column to that table:

Sequel.migration do
  change do
    add_column :articles, :cover_photo_data, :text # or :jsonb


In your model form you can now add form fields for the attachment attribute, and an image tag for the preview:

<div class="form-group">
  <input type="hidden" name="article[cover_photo]" value="<%= @article.cached_cover_photo_data %>" class="upload-hidden">
  <input type="file" name="article[cover_photo]" class="upload-file">
<img class="upload-preview">

The file field will be used for choosing files, and the hidden field for storing uploaded file data and retaining it across form redisplays in case of validation errors.

You should now be able to upload the images via the form, and display them in your views:

<img src="<%= @article.cover_photo_url %>" width=500>

Direct upload

We can now add asynchronous direct uploads to the mix. We'll be using a JavaScript file upload library called Uppy, along with Shrine's upload_endpoint plugin.

Upload endpoint

On the server side we'll need to add an endpoint that accept uploads that Uppy forwards. Shrine's upload_endpoint plugin gives you a complete Rack app that forwards received files to the specified storage. All we need to do is load the plugin and mount the endpoint to the desired path.

# config/shrine.rb

Shrine.plugin :upload_endpoint
# For Roda app
route do |r|
  r.on "upload" do Shrine.upload_endpoint(:cache)
  # ...

# For Rails app (config/routes.rb)
Rails.application.routes.draw do
  mount Shrine.upload_endpoint(:cache) => "/upload"
  # ...


Now we can setup Uppy to do the direct uploads. First we'll pull in the necessary JavaScript and CSS files:

<!DOCTYPE html>
    <script src=""></script>
    <script src=""></script>

    <link href="" rel="stylesheet" />


Now we can add the following JavaScript code which will perform direct uploads to Shrine's upload endpoint when the user selects the file, assigning the results to the hidden attachment field to be submitted:

function fileUpload(fileInput) {
  var imagePreview = document.querySelector('.upload-preview') = 'none' // uppy will add its own file input

  var uppy = Uppy.Core({
      autoProceed: true,
    .use(Uppy.FileInput, {
      target: fileInput.parentNode,
    .use(Uppy.Informer, {
      target: fileInput.parentNode,
    .use(Uppy.ProgressBar, {
      target: imagePreview.parentNode,
    .use(Uppy.ThumbnailGenerator, {
      thumbnailWidth: 400,

  uppy.use(Uppy.XHRUpload, {
    endpoint: '/upload', // Shrine's upload endpoint

  uppy.on('upload-success', function (file, response) {
    // read uploaded file data from the upload endpoint response
    var uploadedFileData = JSON.stringify(response.body)

    // set hidden field value to the uploaded file data so that it's submitted with the form as the attachment
    var hiddenInput = fileInput.parentNode.querySelector('.upload-hidden')
    hiddenInput.value = uploadedFileData

  uppy.on('thumbnail:generated', function (file, preview) {
    imagePreview.src = preview

  return uppy

document.querySelectorAll('.upload-file').forEach(function (fileInput) {

And that's it, now when a file is selected it will be asynchronously uploaded to your app. During the upload a nice progress bar will be displayed, and when the upload finishes an image preview will be shown.

See also

You can’t perform that action at this time.