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

Handling File Uploads for a modern developer

Handling File Uploads for a modern developer

In Ruby we have an abundance of options for handling file attachments. Unfortunately, most file attachment gems fall short when it comes to tailoring the attachment flow to your needs and preferences, often leaving your app with suboptimal user experience.

This talk will introduce the Shrine gem and go over some of the modern best practices for handling file attachments. We will cover topics such as image processing (“Is ImageMagick always the best option?”), asynchronous file uploads (“Which JavaScript solution should I choose?”), and handling large uploads (“How do I help users that have a flaky internet connection?”).

Janko Marohnić

September 07, 2019
Tweet

More Decks by Janko Marohnić

Other Decks in Programming

Transcript

  1. Janko Marohnic • from Croatia ", living in Czechia #

    • Ruby off Rails developer • creator of Shrine @janko @jankomarohnic
  2. Active Storage Shrine Rails & Active Record only Rails, Hanami,

    Sinatra, Roda... Active Record, Sequel, ROM... framework library integrated experience more control more opinionated more features
  3. class PhotosController < AC::Base def create photo = Photo.new(photo_params) if

    photo.valid? photo.save # ... else # ... end end end
  4. class ImageUploader < Shrine TYPES = %w[image/jpeg image/png ...] EXTENSIONS

    = %w[jpg jpeg png ...] Attacher.validate do validate_max_size 10*1024*1024 validate_mime_type TYPES validate_extension EXTENSIONS end end
  5. class PhotosController < AC::Base def create file = params[:photo][:image] file.read

    #=> "<?php ... ?>" file.filename #=> "nature.jpg" file.content_type #=> "image/jpeg" photo = Photo.new(image: file) photo.image.mime_type #=> "text/x-php" photo.valid? #=> false end end MIME Type
  6. https://www.bamsoftware.com/hacks/deflate.html $ identify spark.png ... 225000x225000 ... 6.1MB Attacher.validate do

    # ... if validate_mime_type IMAGE_TYPES validate_max_dimensions [5000, 5000] end end Image Dimensions
  7. class VideoUploader < Shrine add_metadata :duration do |file| FFMPEG::Movie.new(file.path).duration end

    Attacher.validate do if file.duration > 5*60*60 errors << "is too long (max is 5h)" end end end Custom Metadata
  8. { "id": "b93777246f10e509a553.mp4", "storage": "store", "metadata": { "size": 234837, "filename":

    "matrix.mp4", "mime_type": "video/mp4", "duration": 9000, ... } } SELECT video_data FROM movies;
  9. magick = ImageProcessing::MiniMagick
 magick = magick.source(image) magick.resize_to_limit! 500, 500 magick.resize_to_fit!

    500, 500 magick.resize_to_fill! 500, 500 magick.resize_and_pad! 500, 500 $ gem install image_processing
  10. thumbnail = ImageProcessing::MiniMagick
 .source(image) .resize_to_limit!(500, 500) $ gem install image_processing

    $ convert source.jpg -auto-orient -resize 500x500> -sharpen 0x1 destination.jpg
  11. thumbnail = ImageProcessing::MiniMagick
 .source(image) .resize_to_limit!(500, 500) $ gem install image_processing

    $ convert source.jpg -auto-orient -resize 500x500> -sharpen 0x1 destination.jpg
  12. THUMBNAILS = { xs: [200, 200], s: [400, 400], m:

    [600, 600], l: [800, 800], xl: [1200, 1200], } THUMBNAILS.each do |name, (width, height)| ImageProcessing::Backend .source(original) .resize_to_limit!(width, height) end
  13. Shrine https://.../thumbnail/600/400/... photo.image.derivation_url( :thumbnail, # <-- name 600, 400 #

    <-- args ) Shrine.derivation :thumbnail do |file, w, h| ImageProcessing::Vips.source(file) .strip .saver(quality: 85, interlace: "JPEG") .resize_to_limit!(w.to_i, h.to_i) end
  14. class ImageUploader < Shrine derivatives_processor :thumbs do |file| vips =

    ImageProcessing::Vips.source(file) { large: vips.resize_to_fit!(800, 800), medium: vips.resize_to_fit!(500, 500), small: vips.resize_to_fit!(300, 300) } end end photo = Photo.new(image: file) photo.image_derivatives!(:thumbs) photo.image_derivatives #=> # { large: #<Shrine::File @id="ld7.jpg">, # medium: #<Shrine::File @id="jg4.jpg">, # small: #<Shrine::File @id="02k.jpg"> }
  15. class VideoUploader < Shrine derivatives_processor :transcode do |file| movie =

    FMMPEG::Movie.new(file.path) transcoded = Tempfile.new(["", ".mp4"]) screenshot = Tempfile.new(["", ".jpg"]) movie.transcode(transcoded.path) movie.screenshot(screenshot.path) { transcoded: transcoded, screenshot: screenshot } end end
  16. { "url": "...", "fields": "..." } submit { "id": "xyz.jpg",

    ... } App S3 GET /s3/params POST s3.aws.com
  17. tus.io • HTTP protocol for resumable uploads • Numerous implementations:

    • Client – JavaScript, iOS, Android, ... • Server – Ruby, Node, Go, Python, Java, PHP, ...
  18. Recap • Validate and persist metadata • Processing "on attachment"

    or "on-the-fly" ImageProcessing – ImageMagick & libvips • Direct uploads Uppy simple uploads (to your app or to cloud) resumable uploads (S3 multipart upload or tus.io)
  19. • https://shrinerb.com/ • https://github.com/janko/image_processing • https://libvips.github.io/libvips/ • https://uppy.io/ • https://tus.io/

    • https://github.com/janko/tus-ruby-server • https://github.com/janko/uppy-s3_multipart • https://twin.github.io