Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Shrine: Handle file uploads like it's 2017

Shrine: Handle file uploads like it's 2017

Janko Marohnić

February 22, 2017
Tweet

More Decks by Janko Marohnić

Other Decks in Programming

Transcript

  1. Choose a file… picture.jpg Submit POST /photos Content-Type: multipart/form-data …

    […file content…] { name: "file", filename: "picture.jpg", type: "image/jpeg", tempfile: #<Tempfile:/var/…/RackMultipart-286> } Rack
  2. Choose a file… picture.jpg Submit POST /albums Content-Type: multipart/form-data …

    […file content…] #<ActionDispatch::Http::UploadedFile @original_filename=“picture.jpg”, @content_type=“image/jpeg”, @tempfile=#<Tempfile:/var/…/RackMultipart-286>> Rails
  3. Shrine.storages = { store: Shrine::Storage::FileSystem.new( "public", prefix: "uploads/store") } Shrine.plugin

    :sequel # or :activerecord class ImageUploader < Shrine # image attachment logic end class Photo < Sequel::Model include ImageUploader[:image] end
  4. photo = Photo.create(image: File.open("…")) photo.image_data #=> { "storage": "store", "id":

    "fd9sdf0ag32kf1s.jpg", "metadata": { "filename": "nature.jpg", "size": 883954, "mime_type": "image/jpeg" } }
  5. Choose a file… picture.jpg Submit Album title Choose a file…

    No file selected Submit Album title can’t be blank (Waiting for file to be uploaded…)
  6. • User has a better idea how long upload will

    take • User can fill in other fields while file is uploading • Multiple files can be uploaded in parallel Benefits
  7. • Receiving file uploads uses server resources • Stored files

    are served through the app • Doesn’t work with multiple servers • Heroku doesn’t support storing on disk • Heroku has a 30-second request timeout Downsides of filesystem
  8. Shrine.storages = { cache: Shrine::Storage::S3.new(…), store: Shrine::Storage::S3.new(…), } Shrine.storages =

    { cache: Shrine::Storage::FileSystem.new(…), store: Shrine::Storage::FileSystem.new(…), }
  9. 1. GET /images/cache/presign { "url": "https://my-bucket.s3.amazonaws.com", "fields": { "key": "b7d575850ba61b44c8a9ff889dfdb14d88cdc25",

    "policy": "eyJleHBpcmF0aW9uIjoiMjAxNS0QwMToxMTo...", "x-amz-credential": "5TQ/eu-west-1/s3/aws4_request", "x-amz-algorithm": "AWS4-HMAC-SHA256", "x-amz-date": "20151024T001129Z", "x-amz-signature": "c1eb634f83f96b69bd675f535b3..." } } 2. POST https://my-bucket.s3.amazonaws.com Content-Type: multipart/form-data […file content…]
  10. • Flaky connection terminates the upload • User needs to

    change location during upload • Computer freezes in the middle of upload Large files => User needs to retry the whole upload
  11. Chunking 5MB 5MB 5MB 5MB 5MB 1 2 3 4

    5 ✅ ✅ 17% 54% 32%
  12. Chunking 5MB 5MB 5MB 5MB 5MB 1 2 3 4

    5 ✅ ✅ ✅ ✅ ✅
  13. • S3 & Azure • Concurrent chunking • Numerous other

    features • Well-designed protocol • iOS & Android • Ruby implementation
  14. # config.ru require "tus/server" map("/files") { run Tus::Server } janko-m/tus-ruby-server

    Shrine.storages = { cache: Shrine::Storage::Url.new, store: Shrine::Storage::S3.new(…), } janko-m/shrine-url
  15. • Uploads can be resumed in case of interruption •

    Concurrent chunking can speed up the upload • Checksums can ensure that no bytes were lost Benefits
  16. class PromoteJob include Sidekiq::Worker def perform(data) Shrine::Attacher.promote(data) end end Shrine.plugin

    :backgrounding Shrine::Attacher.promote do |data| PromoteJob.perform_async(data) end mperham/sidekiq
  17. class PromoteJob include Sidekiq::Worker def perform(data) Shrine::Attacher.promote(data) end end Shrine.plugin

    :backgrounding Shrine::Attacher.promote do |data| PromoteJob.perform_in(3, data) end mperham/sidekiq
  18. class PromoteJob include Sidekiq::Worker def perform(data) ActiveRecord::Base.with_connection do Shrine::Attacher.promote(data) end

    end end Shrine.plugin :backgrounding Shrine::Attacher.promote do |data| PromoteJob.perform_async(data) end mperham/sidekiq
  19. class PromoteJob include SuckerPunch::Job def perform(data) Shrine::Attacher.promote(data) end end Shrine.plugin

    :backgrounding Shrine::Attacher.promote do |data| PromoteJob.perform_async(data) end brandonhilkert/sucker_punch
  20. class PromoteJob < RocketJob::Job key :data, Hash def perform(data) Shrine::Attacher.promote(data)

    end end Shrine.plugin :backgrounding Shrine::Attacher.promote do |data| PromoteJob.create!(data: data) end rocketjob/rocketjob
  21. class DeleteJob < RocketJob::Job key :data, Hash def perform(data) Shrine::Attacher.delete(data)

    end end Shrine.plugin :backgrounding Shrine::Attacher.promote { |data| … } Shrine::Attacher.delete { |data| … } rocketjob/rocketjob
  22. album.cover_photo #=> nil album.cover_photo = File.open("...") album.save # background job

    is kicked off album.cover_photo.storage_key #=> "cache" # … background job finishes album.reload album.cover_photo.storage_key #=> "store"
  23. Benefits • Fast for the user • Removes impact on

    request throughput • Can be scaled individually • Can automatically retry timeouts • Easier to manually retry failures that require action • Easier to monitor performance