/Resumable-Uploads

Resumable, chunked, Uploads with HTML5 and Rails 3.1+

Primary LanguageRubyGNU Lesser General Public License v3.0LGPL-3.0

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: https://github.com/JangoSteve/remotipart

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://github.com/stakach/Resumable-Uploads.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
		#
		session[:user]
	end
	
	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 = Model.new(result)
		me.save
		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
			FileUtils.mv(result[:filepath], me.storage_location)	# etc.. This should be in the model in after_create
			return true
		end
	end
	
	#
	# 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 = Proc.new {|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');

		draghotspot.resumable({
			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 () {
					draghotspot.trigger('cancelAll');
				};
				diag.dialog({
					modal: true,
					buttons: thebuttons,
					close: function () {
						if (window.onbeforeunload != null)
							draghotspot.trigger('cancelAll');
					}
				});
				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 });
				else
					$.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
				diag.dialog("close").dialog("destroy");
				$.noticeAdd({ text: (total - failures) + " items successfully uploaded", stayTime: 6000 });
			}
		}).css({
			//
			// 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
			//
		});
	}


Credits

The client side code was refactored and inspired by: http://code.google.com/p/jquery-html5-upload/