Resumable-Uploads
Resumable-Uploads copied to clipboard
Resumable, chunked, Uploads with HTML5 and Rails 3.1+
h1. 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
h2. How it works
You drag and drop a large file for uploading (or use file input, yawn)
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
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
The JavaScript looks at the response then sends the next part of the file
The server appends the part to the file then requests the next part
and so on...
I've defined the part size to be a constant 1mb (1024 x 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
h2. 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
h3. Basic Usage
Create an entry in your gem file for it: gem 'resolute', :git => 'git://github.com/stakach/Resumable-Uploads.git'
Create an initializer for the configuration: config/initializers/uploads.rb (for example)
#* Enter config (outlined in the next section)
Update your routes
Run migrations
#* First need to copy over the migrations: rake railties:install:migrations #* Then: rake db:migrate
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@
Configure the client side jQuery as you wish (outlined below)
h3. 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"
h3. 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 = $('').html('');
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()',
'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
//
});
}
h2. Credits
The client side code was refactored and inspired by: http://code.google.com/p/jquery-html5-upload/