Adding Direct App Uploads (original) (raw)
This walkthrough shows how to add asynchronous uploads to a Rails app. The flow will go like this:
- User selects file(s)
- Files are uploaded asynchronously to an upload endpoint on your app
- Uploaded file JSON data is written to a hidden field
- Form is submitted instantaneously as it only has to submit the JSON data
- JSON data is assigned to the Shrine attachment attribute
1. Installation
Add Shrine to the Gemfile:
Gemfile
...
gem "shrine", "~> 3.0"
and run bundle install
.
2. Initializer
Create an initializer which configures your storage and loads default plugins:
config/initializers/shrine.rb
require "shrine" require "shrine/storage/file_system"
Shrine.storages = { cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"), }
Shrine.plugin :activerecord # load Active Record integration Shrine.plugin :cached_attachment_data # for retaining cached file on form redisplays Shrine.plugin :restore_cached_data # refresh metadata for cached files
3. Migration
Add the <attachment>_data
text or JSON column to the table to which you want to add the attachment:
$ rails generate migration add_image_data_to_photos image_data:text # or :jsonb
This should generate the following migration:
class AddImageDataToPhotos < ActiveRecord::Migration def change add_column :photos, :image_data, :text # or :jsonb end end
Run rails db:migrate
to apply the migration.
4. Attachment
Create an uploader for the types of files you'll be uploading:
app/uploaders/image_uploader.rb
class ImageUploader < Shrine end
and add an attachment attribute to your model:
app/models/photo.rb
class Photo < ApplicationRecord include ImageUploader::Attachment(:image)
validates_presence_of :image end
5. Form
In your form you can now add form fields for the attachment attribute, and an image tag for the preview:
<%= form_for @photo do |f| %>
In your controller make sure to allow the attachment param:
app/controllers/photos_controller.rb
class PhotosController < ApplicationController
...
def create @photo = Photo.new(photo_params)
if @photo.save
redirect_to @photo
else
render :new
end
end
...
private
def photo_params params.require(:photo).permit(..., :image) # permit attachment param end end
Now you should have working synchronous file uploads.
6. Direct upload
We can now add asynchronous direct uploads to the mix. We'll be using Uppyand its XHRUpload plugin, which will upload selected files to Shrine'supload endpoint.
6a. Upload endpoint
We'll first configure the upload_enpdoint
plugin and mount it in our routes:
config/initializers/shrine.rb
...
Shrine.plugin :upload_endpoint, url: true
config/routes.rb
Rails.application.routes.draw do
...
mount Shrine.upload_endpoint(:cache) => "/upload" end
6b. Uppy
Now we can setup Uppy to do the direct uploads. First we'll add the package to our bundle (we're assuming you're using webpacker):
Now we can setup direct uploads, where selected files will go to Shrine's upload endpoint, and upload result will be written to the hidden attachment field:
// app/javascript/fileUpload.js
import 'uppy/dist/uppy.min.css'
import { Core, FileInput, Informer, ProgressBar, ThumbnailGenerator, XHRUpload, } from 'uppy'
function fileUpload(fileInput) { const hiddenInput = document.querySelector('.upload-data'), imagePreview = document.querySelector('.upload-preview img'), formGroup = fileInput.parentNode
// remove our file input in favour of Uppy's formGroup.removeChild(fileInput)
const uppy = Core({ autoProceed: true, }) .use(FileInput, { target: formGroup, }) .use(Informer, { target: formGroup, }) .use(ProgressBar, { target: imagePreview.parentNode, }) .use(ThumbnailGenerator, { thumbnailHeight: 600, }) .use(XHRUpload, { endpoint: '/upload', // path to the upload endpoint })
uppy.on('thumbnail:generated', (file, preview) => { // show image preview while the file is being uploaded imagePreview.src = preview })
uppy.on('upload-success', (file, response) => { // retrieve uploaded file data const uploadedFileData = response.body['data']
// set hidden field value to the uploaded file data so that it's submitted
// with the form as the attachment
hiddenInput.value = JSON.stringify(uploadedFileData)
}) }
export default fileUpload
// app/javascript/packs/application.js // ... import fileUpload from 'fileUpload'
// listen on 'turbolinks:load' instead of 'DOMContentLoaded' if using Turbolinks document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.upload-file').forEach(fileInput => { fileUpload(fileInput) }) })
And add the following CSS:
/* app/assets/stylesheets/application.css / / ... */ .upload-preview img { display: block; max-width: 100%; }
.upload-preview { margin-bottom: 10px; display: inline-block; height: 300px; }
img[src=""] { visibility: hidden; }
And that's it, now when a file is selected it will be asynchronously uploaded directly to S3, accompanied by an image preview and a nice progress bar.