Slide 1

Slide 1 text

Shrine File Upload Toolkit janko-m @jankomarohnic

Slide 2

Slide 2 text

• LOC = 470 (base) + 1550 (plugins) • 1 tiny dependency • streaming uploads & downloads • closes files and deletes tempfiles • asynchronous processing & uploads & deletes • simple and flexible design • support for Sequel • security

Slide 3

Slide 3 text

Design

Slide 4

Slide 4 text

Storage class MyStorage # PORO def upload(io, id) # ... end def url(id) # ... end def exists?(id) # ... end def delete(id) # ... end # ... end

Slide 5

Slide 5 text

Shrine.storages = { disk: Shrine::Storage::FileSystem.new("uploads") } uploader = Shrine.new(:disk) uploader.storage #=> # uploaded_file = uploader.upload(image) uploaded_file #=> # uploaded_file.data #=> # { # "storage" => "disk", # "id" => "9260ea09d8effd.jpg", # "metadata" => { # "filename" => "image.jpg", # "size" => 819344, # "mime_type" => "image/jpeg", # }, # } Uploader

Slide 6

Slide 6 text

uploaded_file.url #=> "uploads/9260ea09d8effd.jpg" uploaded_file.metadata #=> {...} uploaded_file.read #=> "..." uploaded_file.exists? #=> true uploaded_file.download #=> # uploaded_file.delete ... Uploaded File

Slide 7

Slide 7 text

Shrine.storages = { cache: Shrine::Storage::FileSystem.new(...), store: Shrine::Storage::FileSystem.new(...), } Shrine.plugin :sequel # or :activerecord class ImageUploader < Shrine # ... end class User < Sequel::Model include ImageUploader[:avatar] end Attachment add_column :users, :avatar_data, :text

Slide 8

Slide 8 text

user = User.create(avatar: File.open(“avatar.jpg")) user.avatar_data #=> "{\"storage\":\"store\",\"id\": \"sg00943klsg8dd.jpg\",\"metadata\":{...}}" user.avatar #=> # user.avatar.url #=> "uploads/9dkasd920s.jpg" user.destroy user.avatar.exists? #=> false Attacher

Slide 9

Slide 9 text

Processing

Slide 10

Slide 10 text

class ImageUploader < Shrine def process(io, context) if context[:phase] == :store # ... end end end

Slide 11

Slide 11 text

require "image_processing/mini_magick" class ImageUploader < Shrine include ImageProcessing::MiniMagick def process(io, context) if context[:phase] == :store resize_to_limit(io.download, 800, 800) end end end

Slide 12

Slide 12 text

require "image_processing/mini_magick" class ImageUploader < Shrine include ImageProcessing::MiniMagick plugin :versions, names: [:small, :medium, :large] def process(io, context) if context[:phase] == :store size_800 = resize_to_limit(io.download, 800, 800) size_500 = resize_to_limit(size_800, 500, 500) size_300 = resize_to_limit(size_500, 300, 300) {small: size_300, medium: size_500, large: size_800} end end end

Slide 13

Slide 13 text

user.avatar_data #=> # { # "large": { # "id": "lg043.jpg", # "storage": "store", # "metadata" : {"size": 94832, ...} # }, # "medium": { # "id": "kd9fk.jpg", # "storage": "store", # "metadata" : {"size": 39291, ...} # }, # "small": { # "id": "932fl.jpg", # "storage": "store", # "metadata" : {"size": 10582, ...} # } # }

Slide 14

Slide 14 text

user.avatar.class #=> Hash user.avatar #=> # { # :large => #, # :medium => #, # :small => #, # } user.avatar[:large].size #=> 94832 user.avatar[:medium].size #=> 39291 user.avatar[:small].size #=> 10582

Slide 15

Slide 15 text

Direct upload

Slide 16

Slide 16 text

Choose a file Fill in other fields Submit the form Wait for file to be uploaded Get redirected

Slide 17

Slide 17 text

Choose a file Fill in other fields Submit the form No waiting Get redirected File uploads using AJAX

Slide 18

Slide 18 text

# config/routes.rb Rails.application.routes.draw do mount ImageUploader::UploadEndpoint => "/images" end Shrine.plugin :direct_upload // with jQuery-File-Upload $('[type="file"]').fileupload({ url: '/images/cache/avatar', paramName: 'file', add: (e, data) => { /* Disable submit button */ }, progress: (e, data) => { /* Add progress bar */ }, done: (e, data) => { /* Fill hidden field */ }, }); Generic

Slide 19

Slide 19 text

Shrine.storages = { cache: Shrine::Storage::S3.new(...), store: Shrine::Storage::S3.new(...), } Shrine.plugin :direct_upload, presign: true GET /images/cache/presign { "url": "https://my-bucket.s3-eu-west-1.amazonaws.com", "fields": { "key": "image.jpg", "x-amz-credential": "...", "x-amz-algorithm": "...", "x-amz-date": "...", "x-amz-signature": "..." } } Amazon S3

Slide 20

Slide 20 text

Backgrounding

Slide 21

Slide 21 text

• Processing, uploading and deleting • Graceful degradation • Supports any backgrounding library • Handles parallel jobs • Adaptation to changes • Debuggable failures • Security

Slide 22

Slide 22 text

Shrine.plugin :backgrounding Shrine::Attacher.promote { |data| UploadJob.perform_async(data) } Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) } class UploadJob include Sidekiq::Worker def perform(data) Shrine::Attacher.promote(data) end end class DeleteJob include Sidekiq::Worker def perform(data) Shrine::Attacher.delete(data) end end

Slide 23

Slide 23 text

class ImageUploader < Shrine plugin :versions, names: [...] def process(io, context) case context[:phase] when :recache # process cheap versions in foreground when :store # process expensive versions in background end end end Shrine.plugin :recache

Slide 24

Slide 24 text

Logging

Slide 25

Slide 25 text

Shrine.plugin :logging STORE[cache] ImageUploader[:avatar] User[29543] 1 file (0.1s) PROCESS[store]: ImageUploader[:avatar] User[29543] 1-3 files (0.22s) DELETE[destroy]: ImageUploader[:avatar] User[29543] 3 files (0.07s) Shrine.plugin :logging, format: :json {“action”:”upload”, “phase”:”cache”, “duration”: 0.16, ...} Shrine.plugin :logging, format: :heroku action=upload phase=cache files=1 duration=0.16 record_class=User

Slide 26

Slide 26 text

Other plugins • determine_mime_type
 – determines actual MIME type from file contents • store_dimensions
 – extracts and stores image width & height • validation_helpers
 – validation of filesize, extension, MIME type, dimensions • parallelize
 – parallelizes uploading and deleting versions • … (many more)

Slide 27

Slide 27 text

Storages • FileSystem • S3 • Cloudinary • Imgix • Fog • Flickr • SQL • GridFS

Slide 28

Slide 28 text

• http://github.com/janko-m/shrine • http://shrinerb.com (website) • http://github.com/janko-m/shrine-example • http://twin.github.io/introducing-shrine/ • http://twin.github.io/file-uploads-asynchronous- world/