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
- 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 × 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
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
- 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)
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"
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
//
});
}
The client side code was refactored and inspired by: http://code.google.com/p/jquery-html5-upload/