Resumable, chunked, Uploads with HTML5 and Rails 3.1+
Switch branches/tags
Nothing to show
Clone or download
Pull request Compare This branch is even with stakach:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Resumable File Uploads for HTML5

These are the skeleton files for providing resumable file uploads in Rails.
Works in

  • Chrome 8+
  • Safari 5+
  • Firefox 7+
  • IE 10+

For ajax upload fallback in older browsers use:

How it works

  1. You drag and drop a large file for uploading (or use file input, yawn)
  2. Using JavaScript FileAPI we send a simple fingerprint of the file to the server
    • filename, file size, modified date – the user is also taken into account, determined by the current session data
  3. The server looks for an existing matching file
    • If not found it creates a new one
    • Then it returns the file_id and the part of the file we are up to
  4. The JavaScript looks at the response then sends the next part of the file
  5. The server appends the part to the file then requests the next part
  6. and so on…

I’ve defined the part size to be a constant 1mb (1024 × 1024). Feel free to adjust this as you see fit.

Other features include:

  • Appending more files while an upload is taking place
  • Pausing and resuming uploads (to the nearest mb) – when blobs are supported
  • Client side filtering of files that can be uploaded where desirable

Using it with Rails 3.1

I’ve made the following assumptions

  • You are validating users before letting them upload
    • There are callbacks to obtain this information
  • Once the upload has occured you will be performing post processing of some sort
    • Creating a DB entry to manage the location of the file, as the most basic example

Basic Usage

  1. Create an entry in your gem file for it: gem ‘resolute’, :git => ‘git://’
  2. Create an initializer for the configuration: config/initializers/uploads.rb (for example)
    • Enter config (outlined in the next section)
  3. Update your routes
  4. Run migrations
    • First need to copy over the migrations: rake railties:install:migrations
    • Then: rake db:migrate
  5. Include the javascript
    • In the head section of your document include: <%= javascript_include_tag resumables.js %>
    • Or as an include in your application.js //= require resolute/resumables
  6. Configure the client side jQuery as you wish (outlined below)

Engine Config

In your initializer:

	Resolute.current_user do
		# This is run in the context of the controller
		#	Return a unique identifier that can be stringified
	Resolute.upload_completed  do  |result|
		#	Result is a hash with the following fields
		#		:user	(user identifier defined by current_user above)
		#		:filename (original, unmodified, filename as sent from client side)
		#		:filepath (relational path to the file)
		#		:params (any custom parameters from the client side)
		#		:resumable - the resumable db entry. Will be automatically destroyed on success, not on failure.
		#						This provides you the opportunity to destroy it if you like. Will be nil if it is a regular upload.
		me =
		if me.new_record?
			# If the uploaded file is not required delete it and destroy resumable here too
			return me.errors	# Provide the client side with some information
		else[:filepath], me.storage_location)	# etc.. This should be in the model in after_create
			return true
	# These are the defaults for the following two options:
	Resolute.upload_folder = 'tmp/uploading'	# Folder is created if it doesn't exist
	# Provides a way to prevent an upload as early as possible
	#	Return false or an array of errors if the file type is not supported (determined from filename)
	Resolute.check_supported = {|file_info| return true}	# Can also be defined as a block like above
		# File_info hash contains the following - :user, :filename, :params (any custom parameters from the client side)

The engine also needs to have a base path defined in routes: config/routes.rb
Create an entry where ever you want, the client side defaults to /uploads

	mount Resolute::Engine => "/uploads"

Client side JavaScript

Provides the hooks into your web application to provide upload hotspots (think gmail uploads) and feedback

	$('#file_input, #drop_spot').resumable({
		// Event callbacks (can be set here or bound at any point in time)
		//onStart: return modified file list or undefined,			// Passed: (event, file list)
		//onAppendFiles: return modified file list or undefined,	// Passed: (event, file list)
		//onUploadStarted: returning false will skip the file,		// Passed: (event, name, index, total)
		//onUploadProgress:					// Passed: (event, progress, name, index, total)
		//onUploadFinish:					// Passed: (event, response, name, index, total)
		//onUploadError:					// Passed: (event, name, index, error_code, response_text)
		//	403: Forbidden - not logged in or not your file
		//	406: Not acceptable - could not save, maybe bad params or file format not supported.
		//	422: unprocessable entity - unknown error and could not save.
		//onFinish:							// Passed: (event, total, failures)
		// Configuration options
		baseURL: '/uploads',	// Default
		//additionalParameters: JS Object or function(file) {return {params: 'data'}},
		autostart: true,			// On change if using input box
		autoclear: true,			// Clears the file upload input box once complete
		disableInput: true,			// Prevents an input field being used while uploads are occuring
		retry_part_errors: false,	// Applies only to resumable uploads
		retry_limit: 3,				// Number of part retries before giving up
		halt_on_error: false		// Stop uploading further files?

Here is a real world example using jQuery UI

	if ($.support.filereader && $.support.formdata) {
		var diag = $('<div />').html('<div></div><p></p>');
		var progress = diag.children('div').progressbar({
			value: 0
		var status = diag.children('p');
		var draghotspot = $('#library > div');

			onStart: function (event, files) {
				window.onbeforeunload = confirmExit; // Make sure we warn the user before exiting the page while uploading
				failures = 0;
				progress.progressbar("value", 0);

				var thebuttons = {};
				thebuttons["Cancel"] = function () {
					modal: true,
					buttons: thebuttons,
					close: function () {
						if (window.onbeforeunload != null)
				return true;
			onUploadStarted: function (event, name, index, total) {
				diag.dialog('option', 'title', name);
				status.text('Uploading ' + (index + 1) + ' of ' + total);
			onUploadProgress: function (event, completed, name, index, total) {
				progress.progressbar("value", Math.ceil(completed * 100));
			onUploadError: function(event, name, index, error, messages) {
				if(error == 406)
					$.noticeAdd({ text: "File " + name + " not supported", stayTime: 6000 });
				else if (error == 403)
					$.noticeAdd({ text: "Access denied uploading " + name + ". Try logging off and on again", stayTime: 6000 });
					$.noticeAdd({ text: "Unknown error while processing " + name + ". Please try again", stayTime: 6000 });
			onFinish: function (event, total, failures) {
				window.onbeforeunload = null; 		// No more need for user warning
				$.noticeAdd({ text: (total - failures) + " items successfully uploaded", stayTime: 6000 });
			// Add a background informing drag and drop uploads are avaliable
			'background-image': 'url(<%= asset_path 'drag-background.png' %>)',
			'background-repeat': 'no-repeat',
			'background-position': 'center center'
		}).bind('dragenter dragover', function(){
			// Highlight drag zone here (like gmail)
			return false;	// required here
		}).bind('dragleave drop', function(){
			// Remove highlighting here


The client side code was refactored and inspired by: