Filepicker and Aviary - Image uploading on steroids

We all have been using the same code for uploading images for years, but didn’t you always feel that there is something wrong with it? For every other task like writing texts, picking a date, selecting from lot of choices we have a good tool that can help in implementing such feature and improve the user experience, but file uploads almost always feel a little broken. There are some Flash tools that might help, but they are still not good enough.

Solution

Well, welcome to the world invaded by Filepicker and Aviary. Speaking short, Filepicker is a tool that let the user upload images not only from the computer itself but also from web services such as Facebook or Dropbox. Aviary provides you with a powerful HTML5 editor for manipulating photos. Both of them process the images on their servers and provide you an url for downloading a file. If you only need more powerful uploading you can stick with Filepicker widgets otherwise we need to get our hands dirty with their Javascript APIs (or CoffeeScript as you will see) but is not hard at all.

Working with code

Let’s start with the view:

<%= link_to _("Set avatar"), "#", :'data-avatar' => "set" %%>

<a href= "#" data-avatar= "set" > Set avatar </a>

Nothing fancy here. Classic Rails link_to method, using _('') method for translating with FastGettext. We don’t care about URL because we are going to handle clicks in Javascript so I used "#" as URL. Instead of using css classes or id for such link I prefer to use custom data-* attribute

First, we need to display Filepicker popup for choosing image when our link is clicked.

filepicker = window . filepicker filepicker . setKey "filepicker api key" $ ( document ). ready -> $ ( 'body' ). delegate '[data-avatar="set"]' , 'click' , -> images = filepicker . MIMETYPES . IMAGES filepicker . getFile images , ( url , metadata ) -> console . log ( "Choosen image url: #{ url } " )

I use jQuery delegate because if it was a Single Page Application (SPA Todo app example) or the link is dynamically added via AJAX, it can still be properly handled.

After clicking the user needs to give permission for using data from a service or simply upload file from computer, or even take a photo using computer built-in camera.

It’s time now to run the photo editor when the file is picked instead of just using console.log .

Create instance of the editor:

featherEditor = new Aviary . Feather ( apiKey : "key" apiVersion : 2 onSave : ( imageID , newURL ) -> featherEditor . close () return false )

Use it when file picked:

images = filepicker . MIMETYPES . IMAGES filepicker . getFile images , ( url , metadata ) -> preview = $ ( '[data-avatar="preview"]' )[ 0 ] preview . src = url featherEditor . launch image : preview url : url

When user finishes editing the photo and presses “Save” button, onSave callback is executed. You can save the url value in JS variable or use it to fill some hidden field in a form or send it to the server. However the documentation states that “this image may not yet be ready so you will have to poll this link, or alternatively handle the hi-res image server-side”. This is my biggest disappointment when using those two products. For that reason we are going to use postUrl option so that Aviary will send us a request to this given URL when the image is ready. Obviously you will have to use different value of the setting for development, staging and production environment. In development you can either forward some port from your router (I assume it is publicly available) to your computer or alternatively, if have a server you can use ssh to forward traffic from the server to your local machine.

Forwarding ports with ssh :

ssh user@YOUR_SERVER_IP -R YOUR_SERVER_IP:SOME_SERVER_PORT:127.0.0.1:3000

(Update) Alternatively you can use a solution that does it for you and does not require having custom server. Avdi did a really nice research of http forwarding tools

Launching the editor with postUrl :

featherEditor . launch image : preview url : url postUrl : "http://YOUR_SERVER_IP:SOME_SERVER_PORT/aviary"

Let’s see the controller that is used when Aviary notifies us of the ready image:

class AviaryController < ApplicationController skip_before_filter :verify_authenticity_token , only: [ :create ] def create @user = User . last @user . remote_avatar_url = params [ :url ] @user . save! head :created end end

Find user, set avatar url and save. As simple as that. Where does remote_avatar_url setter comes from ? It is a feature of carrierwave library that I use to store and resize avatars. It can download the remote avatar itself so I do not need to bother myself with that. You can use it with RMagick , mini_magick or vips .

class User < ActiveRecord :: Base mount_uploader :avatar , AvatarUploader end

class AvatarUploader < CarrierWave :: Uploader :: Base include CarrierWave :: RMagick # include CarrierWave::MiniMagick # include CarrierWave::Vips include Sprockets :: Helpers :: RailsHelper include Sprockets :: Helpers :: IsolatedHelper storage :file def store_dir "uploads/ #{ model . class . to_s . underscore } / #{ mounted_as } / #{ model . id } " end def default_url asset_path "avatar/default.png" end version :large do process :resize_to_fit => [ 256 , 256 ] end version :medium do process :resize_to_fit => [ 128 , 128 ] end version :small do process :resize_to_fit => [ 64 , 64 ] end end

The default_url is used when avatar is not set. That way User#avatar method can nicely behave as Null Object. Avdi blogpost about Null Objects is definitely worth reading.

But we don’t want anyone to be capable to send requests to our application and change avatars, do we ? We need to add some protection. And we need to know which user avatar should be changed. Every user will have its own token for updating the avatar. Again, we use custom data-* (exactly data-avatar-token ) attribute to store the token in HTML.

<%= link_to _("Set avatar"), "#", :'data-avatar' => "set", :'data-avatar-token' => AvatarToken.new(current_user).token, :'data-avatar-id' => current_user.id %%>

<a href= "#" data-avatar= "set" data-avatar-token= "akzlEoaW9WhV8djhtWJmCLd9vjQ=" data-avatar-id= "1" > Set avatar </a>

We use postData to store additional metadata that should come with the request from Aviary to our App.

$ ( document ). ready -> $ ( 'body' ). delegate '[data-avatar="set"]' , 'click' , -> self = $ ( this ) token = self . attr ( 'data-avatar-token' ) id = self . attr ( 'data-avatar-id' ) images = filepicker . MIMETYPES . IMAGES filepicker . getFile images , ( url , metadata ) -> preview = $ ( '[data-avatar="preview"]' )[ 0 ] preview . src = url featherEditor . launch image : preview url : url postUrl : "http://IP:PORT/users/ #{ id } /avatar" postData : token : token

Now we can use this data in our controller to verify the request:

class Users :: AvatarsController < ApplicationController skip_before_filter :verify_authenticity_token , only: [ :create ] def create @user = User . find ( params [ :user_id ]) postdata = JSON . parse ( params [ :postdata ]) rescue {} token = postdata [ 'token' ] AvatarToken . new ( @user ). verify! token @user . remote_avatar_url = params [ :url ] @user . save! head :created end end

And this leaves us with the implementation of AvatarToken class.

require 'openssl' require 'base64' class AvatarToken class Invalid < StandardError ; end attr_reader :user delegate :login , to: :user def initialize ( user ) @user = user @algorithm = OpenSSL :: Digest :: Digest . new ( 'sha1' ) end def verify! ( another_token ) unless token == another_token msg = "invalid ' #{ another_token } ' token for user ' #{ login } '" raise Invalid , msg end end def token digest = OpenSSL :: HMAC . digest ( @algorithm , key , id ) Base64 . strict_encode64 ( digest ) end end

What do you compute digest of ? Well, that depends on your application.

One more thing that I would like to do is to always get square images from Aviary. I could not find 100% reliable way of doing that. My trick is to allow users to use only one type of crop ratio and show the crop tool as initial one. However, the user can still press “Cancel” unfortunately.

featherEditor . launch cropPresets : [ '1:1' ] initTool : 'crop'

So the whole JS part looks like this:

filepicker = window . filepicker filepicker . setKey "Filepicker API Key" featherEditor = new Aviary . Feather ( apiKey : "Aviary API Key" apiVersion : 2 onSave : ( imageID , newURL ) -> featherEditor . close () return false ) $ ( document ). ready -> $ ( 'body' ). delegate '[data-avatar="set"]' , 'click' , -> self = $ ( this ) token = self . attr ( 'data-avatar-token' ) id = self . attr ( 'data-avatar-id' ) images = filepicker . MIMETYPES . IMAGES filepicker . getFile images , ( url , metadata ) -> preview = $ ( '[data-avatar="preview"]' )[ 0 ] preview . src = url featherEditor . launch image : preview url : url postUrl : "http://IP:PORT/users/ #{ id } /avatar" postData : token : token fileFormat : 'png' cropPresets : [ '1:1' ] initTool : 'crop' onError : ( errorObj ) -> alert ( errorObj . message + errorObj . code )

Few more notes about good and bad parts of this solution:

Pro:

The concepts behind Filepicker and Aviary are amazing and I believe they will change the web. It’s like ‘Editor as a Service’, ‘Picker as a Service’. What else could be a service ? I would love to use Gliffy editor in my app the same way I use Aviary.

Filepicker can store files directly in S3 so you do not have to keep them. I just prefer to have them on my machine.

Javascripts are available via HTTPS links.

Cons: