Avatar uploads with Meteor

Hi all!

I’m still very new to meteor but making good progress on my first project - I’m really enjoying using the platform - feels like cheating!

However… I’m very interested in using Meteor-Dropzone (https://atmospherejs.com/dbarrett/dropzonejs) (or one of the alternatives) but am hitting some issues I wanted to discuss with the community.

In lieu of any proper examples in the documentation I tracked down this SO question which sheds some light on how to go about using it. However it seems to hinge on using CollectionFS, whose latest build is failing and the docs clearly state:

This branch is under active development right now (2015-03-01). It has bugs and the API may continue to change. Please help test it and fix bugs, but don’t use in production yet.

The GridFS and S3 packages say much the same thing.

So what are the options available to someone who does want to provide image uploads in a production environment? Are there any good resources out there to get me on my way?

Also, while I’m here, I’ll be handling files much smaller than 16Mb, smallish avatars, basically - from my little reading on the subject it seems that GridFs is ideal for larger files - am I better off using a different approach given my needs? I’m probably more comfortable with local storage, as it’s simple and familiar, but I wouldn’t want to miss a trick.

Edit: Have been playing with S3 - although I’m not sure if this is a little overkill for handling avatars. What approaches are others using? (I often just use Gravatar, but in this case I’d like to provide the option to change the image).

2 Likes
3 Likes

IMO the easiest and most reliable way to handle file uploads (including avatars) is to use slingshot with S3.

It may be a little bit overkill as you say but it will save you a lot of time as it’s super easy to set up.

2 Likes

I second the Slingshot recommendation - after having spent what feels like far too long investigating all the various avatar/upload options for Meteor, Slingshot seemed like the best bet. When it actually came to it, the setup was pretty straight-forward (just make sure to follow the guides carefully), and once setup it works beautifully! Also, as an added benefit, even though originally I only needed custom user avatar support, it is now insanely quick and simple to add custom upload of any filetype whatsoever to my app with only a few extra lines of code. Highly recommended.

(btw, nice to see a fellow Symphony alumni exploring Meteor :wink: )

1 Like

Thanks for the recommendation guys, I hadn’t actually come across slingshot! Will have a play.

@firegoby We clearly just have exceptional taste :slight_smile:

2 Likes

I’ve been playing with slingshot this morning and it was a great recommendation!

I’m going to have a hunt after lunch, but have either of you implemented a ‘drop file here’ type interface to your slingshot uploads? There are a few packages out there I should be able to use, but experience always welcome.

@firegoby @elgusto

I haven’t I’m afraid, though there’s a cool package raix:ui-dropped-event that adds a ‘dropped’ event to your Blaze Events helper that should make it trivial to implement a custom version very quickly. (I haven’t done so myself before but it looks like it should just be a case of calling your Slingshot upload method from within the ‘dropped’ event handler).

That was the package I was looking at - will give it a shot soon!

Currently looking at the best way to handle resizing of images before dropping them into S3 - due to the way in which slingshot works it’s not totally straightforward - but http://stackoverflow.com/a/30683815/2050403 looks promising!

For the resize features take a look at this repo: https://github.com/timbrandin/autoform-slingshot

If you need to make it work on mobile, you can use the cordova camera plugin with the allowEdit option. It also works with Android (from 4.4 only I think). It will only be a crop and not really a resize but it is still quite a nice feature.

1 Like

I’ve got jamgold:cropuploader integration on my immediate todo list (it combines slingshot with the jquery cropper plugin) so I’ll let you know how that goes.

1 Like

Oooh, looks interesting, I’ll check that out too - look forward to hearing how you get on with it!

Ok, I didn’t get on so well with jamgold:cropuploader, I got the first bit working easy enough (the uploading a derivative), but couldn’t really get the Cropper functionality to work (I’m working in a React app so YMMV). If you do have a look at it yourself I’d recommend (after reading the docs) jumping straight to the code in the example app in the github repo rather than manually work through the code in the docs, it’ll probably save you a bunch of time.

In the end I’ve decided I’ve dropped enough time on it without enough solid results that I’m going to go in another direction (I wasn’t that keen on the Cropper plugin to begin with). So I’m going to try using JCrop (which I’ve used before with great results) and hopefully using the approach outlined in the StackOverflow post you linked to can get client side cropping working that then just uploads the cropped/resized image over the wire direct to S3. (That’ll be the best solution for the user as they can crop a bit of a local hi-res photo and use that as their avatar with minimal upload bandwidth needed and good for me, no server involvement). Don’t have time to get to all that today though, but if it works I’ll let you know.

1 Like

Awesome, thanks for the update, much appreciated! I may still give it a go, my app’s pretty straight forward at the moment, no react or other front-end frameworks involved (and unlikely to be).

Sorry @elgusto I managed to miss this - will check that out to, cheers!

Edit: This was very simple to set up - assuming no problems down the line I may stick with this one. Shame about the mobile support - but given the context of my app that may fall into ‘nice to have’ - hopefully something that’s resolved at some point though…


@firegoby I’ve just successfully implemented the solution on stackoverflow (I don’t need user cropping so it’s a complete solution for me) and after some fiddling It’s working really nicely actually - thought I’d give it a shot before settling. Because it also deals with image quality as well as resizing I can crunch user supplied images into nice small jpegs which is always a bonus when working with something like S3!

Given that I don’t use Coffeescript OR the ES6 Promises package I did have to do some significant fiddling - but through some fit of chance what I’ve done works. In case you’re in a similar position here’s my event (everything else is standard slingshot):

Template.AccountSettings.events({
  "change #fieldImage": function(event, template) {

    b64ToBlob = function(b64Data, contentType, sliceSize) {
      var byteArray, byteArrays, byteCharacters, byteNumbers, i, offset, slice;
      sliceSize = sliceSize || 512;
      byteCharacters = atob(b64Data);
      byteArrays = [];
      offset = 0;
      while (offset < byteCharacters.length) {
        slice = byteCharacters.slice(offset, offset + sliceSize);
        byteNumbers = (function() {
          var j, ref, results;
          results = [];
          for (i = j = 0, ref = slice.length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
            results.push(slice.charCodeAt(i));
          }
          return results;
        })();
        byteArray = new Uint8Array(byteNumbers);
        byteArrays.push(byteArray);
        offset += sliceSize;
      }
      return new Blob(byteArrays, {
        type: contentType
      });
    };

    var uploader = new Slingshot.Upload("avatars");

    var data;

    var file = event.currentTarget.files[0];

    var data = processImage(file, 300, 300, 0.75, function(data) {
      var match = /^data:([^;]+);base64,(.+)$/.exec(data);
      var name = file.name;
      var type = match[1];
      var b64 = match[2];
      var blob = b64ToBlob(b64, type);
      blob.name = name;
      uploader.send(blob, function (error, downloadUrl) {
        if (error) {
          console.error('Error uploading: ', uploader.xhr.response);
          alert(error);
          // Handle the error
        }
        else {
          // Put this in a method
          Meteor.users.update( {_id: Meteor.userId()} , {$set: {"profile.image": downloadUrl}});
          alert('Image uploaded and saved');
        }
      });
    });

  }
});

Could do with some modularisation but it works. If you spot anything suspicious do let me know though, I have a feeling I’m missing a callback somewhere around the match from converting it from the Promise syntax - but as nothing is throwing an error and it’s working I’m not gonna rock that boat…

Edit: Gah, I hadn’t noticed originally but Clientside Image Manipulation only resizes to one dimension, it doesn’t actually crop to a specific set of dimensions.

Seems there isn’t one package that ticks all the boxes!

1 Like

Many thanks for translating that from Coffeescript! I came back from lunch with the same thing on my mind and you’d already done it, excellent :slight_smile: Worked perfectly, plug-n-play, thanks so much!

After a bit of to-and-fro I managed to get mine working exactly as hoped too - I’m cropping with Jcrop, writing to a Canvas preview, b64ToBlob()'ing that canvas, sending blob to S3. See a little movie, it’s working great! It’s really cool from a user’s perspective to be able to choose a 12MP 5Mb JPG locally, select a crop and only have to upload 150Kb over the wire (and direct to S3 with no server middleman). All with a nice interactive UI!

Yeah, mine needs refactoring too, the code isn’t going to win an awards but like you say, it works (and smoothly too). And there definitely doesn’t seem to be one package that does it all - I just ran through my ‘stack’ for this feature: Meteor, JQuery, Semantic UI, Jcrop, Slingshot, b64ToBlob() And this is still running on Blaze, haven’t even bothered making it a React component – Ah, modern web dev, it’s so straight-forward! :stuck_out_tongue:

1 Like

Good thread guys… would be neat to see a package version of a working solution ; )

1 Like

@firegoby id love to see your finished code if you don’t mind sharing it? I was thinking about it and giving the user control over the crop is better than square cropping it anyway, and i was have issues with getting the progress bar to work…

All it needs now is a droppable interface!

Edit: meant to say great job by the way, the video sold me on it! :slight_smile: and yea it’s a lovely technique doing it all client side.

2 Likes

@nathanhornby @spherop - I can’t share my finished code (it wouldn’t really help anyway, plus it’s not finished yet!) BUT I did go through and stripped out all my app-specific stuff to create a bare-bones implementation with all the same functionality.

Here’s the gist

This code wasn’t great to start with, it’ll be even less so now I’ve gone through it and manually converted from ES6 back to ES5 (never thought I’d have to do that! :stuck_out_tongue: ) and quickly stripped out all my app-specific code. This version works on my machine here but obviously haven’t tested it more, so be warned! Also renamed all the DOM references to IDs with the form #jcrop-<element>

There are numerous places I’ll be going through and rewriting this but my own code is only going to become more app-specific from here on out so this seems like the best point to strip things down to their bare-basics, hence this less than perfect release. But to be fair though, there should be everything you need here, and it makes a reasonable place for people to develop from in the future.

What you should get…

Pre-requisites

  1. You need an existing working Slingshot setup.

  2. You need to install the package mrt:jquery-jcrop with meteor add mrt:jquery-jcrop

  3. Go through jcrop.js searching for TODO: comments and that’s where to update with your own settings (there aren’t very many).

What happens…

  1. User selects file

  2. Change in file element triggers event handler 'change #jcrop-file'

  3. If all goes ok that un-hides #jcrop-step2 with the Jcrop instance initialized inside

  4. User crops image

  5. onChange handler for Jcrop updates the #jcrop-canvas element

  6. User clicks Upload

  7. Canvas is turned into a base64URL, that gets converted into a Blob, blob gets uploaded by Slingshot to S3, reporting progress as it goes via uploader ReactiveVar

Notes

  1. I don’t do semi-colons, deal with it :smiley:

  2. #jcrop-canvas is 150px x 150px on page, but 300 x 300 canvas (and upload) because I wanted @2x retina support. Tweak for your own needs.

  3. You’ll need to implement your own progress bar CSS (mine came from Semantic UI) the code is all there to power one, just needs some styles to actually show it. The text percentage readout does work out of the box though.

  4. Don’t even get me started on the mega switch statement in label() template helper, lol :smiley:

Best of luck with it, let me know how you get on

P.S. - Oh and a droppable interface shouldn’t be hard at all, I haven’t done it but a raix:ui-dropped-event wired up to feed its first dropped file into the first few lines of the 'change #jcrop-file' event handler should do it.

4 Likes

Thanks for sharing all the deets …

1 Like

Hugely helpful @firegoby - look forward to going over this to tomorrow, thanks!

Edit: Darn, looks like I won’t have a chance to play with it today - will update when I get a chance!