mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
feature #3194: Change image upload to use resumable library
Now images are uploaded in chunks and they are retried when an error happens
This commit is contained in:
parent
3df4f4f995
commit
0e15631291
@ -97,8 +97,9 @@ var create_image_tmpl ='<div class="row create_image_header">\
|
||||
</div>\
|
||||
</div>\
|
||||
<div class="row">\
|
||||
<div id="file-uploader" class="large-12 columns text-center">\
|
||||
</div>\
|
||||
<div id="file-uploader" class="large-12 columns text-center">'+
|
||||
tr("Click to choose image")+
|
||||
'</div>\
|
||||
</div>\
|
||||
<div class="img_size row">\
|
||||
<div class="large-6 columns">\
|
||||
@ -925,61 +926,67 @@ function initialize_create_image_dialog(dialog) {
|
||||
|
||||
var img_obj;
|
||||
|
||||
// Upload is handled by FileUploader vendor plugin
|
||||
var uploader = new qq.FileUploaderBasic({
|
||||
button: $('#file-uploader',dialog)[0],
|
||||
action: 'upload',
|
||||
multiple: false,
|
||||
params: {},
|
||||
sizeLimit: 0,
|
||||
showMessage: function(message){
|
||||
//notifyMessage(message);
|
||||
},
|
||||
onSubmit: function(id, fileName){
|
||||
uploader.setParams({
|
||||
csrftoken: csrftoken,
|
||||
img : JSON.stringify(img_obj),
|
||||
file: fileName
|
||||
});
|
||||
|
||||
$('#upload_progress_bars').append('<div id="'+fileName+'progressBar" class="row" style="margin-bottom:10px">\
|
||||
<div class="large-2 columns dataTables_info">\
|
||||
'+tr("Uploading...")+'\
|
||||
</div>\
|
||||
<div class="large-10 columns">\
|
||||
<div id="upload_progress_container" class="progress nine radius" style="height:25px !important">\
|
||||
<span class="meter" style="width:0%"></span>\
|
||||
</div>\
|
||||
<div class="progress-text" style="margin-left:15px">'+fileName+'</div>\
|
||||
</div>\
|
||||
</div>');
|
||||
},
|
||||
onProgress: function(id, fileName, loaded, total){
|
||||
$('span.meter', $('div[id="'+fileName+'progressBar"]')).css('width', Math.floor(loaded*100/total)+'%')
|
||||
},
|
||||
onComplete: function(id, fileName, responseJSON){
|
||||
|
||||
if (uploader._handler._xhrs[id] &&
|
||||
uploader._handler._xhrs[id].status == 500) {
|
||||
|
||||
onError({}, JSON.parse(uploader._handler._xhrs[id].response) )
|
||||
$('div[id="'+fileName+'progressBar"]').remove();
|
||||
} else {
|
||||
notifyMessage("Image uploaded correctly");
|
||||
$('div[id="'+fileName+'progressBar"]').remove();
|
||||
Sunstone.runAction("Image.refresh");
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
onCancel: function(id, fileName){
|
||||
var uploader = new Resumable({
|
||||
target: '/upload_chunk',
|
||||
chunkSize: 10*1024*1024,
|
||||
maxFiles: 1,
|
||||
testChunks: false,
|
||||
query: {
|
||||
csrftoken: csrftoken
|
||||
}
|
||||
});
|
||||
|
||||
uploader.assignBrowse($('#file-uploader',dialog)[0]);
|
||||
|
||||
var fileName = '';
|
||||
var file_input = false;
|
||||
uploader._button._options.onChange = function(input) {
|
||||
file_input = input; return false;
|
||||
};
|
||||
|
||||
uploader.on('fileAdded', function(file){
|
||||
fileName = file.fileName;
|
||||
file_input = fileName;
|
||||
});
|
||||
|
||||
uploader.on('uploadStart', function() {
|
||||
$('#upload_progress_bars').append('<div id="'+fileName+'progressBar" class="row" style="margin-bottom:10px">\
|
||||
<div id="'+fileName+'-info" class="large-2 columns dataTables_info">\
|
||||
'+tr("Uploading...")+'\
|
||||
</div>\
|
||||
<div class="large-10 columns">\
|
||||
<div id="upload_progress_container" class="progress nine radius" style="height:25px !important">\
|
||||
<span class="meter" style="width:0%"></span>\
|
||||
</div>\
|
||||
<div class="progress-text" style="margin-left:15px">'+fileName+'</div>\
|
||||
</div>\
|
||||
</div>');
|
||||
});
|
||||
|
||||
uploader.on('progress', function() {
|
||||
$('span.meter', $('div[id="'+fileName+'progressBar"]')).css('width', uploader.progress()*100.0+'%')
|
||||
});
|
||||
|
||||
uploader.on('fileSuccess', function(file) {
|
||||
$('div[id="'+fileName+'-info"]').text(tr('Registering in OpenNebula'));
|
||||
$.ajax({
|
||||
url: '/upload',
|
||||
type: "POST",
|
||||
data: {
|
||||
csrftoken: csrftoken,
|
||||
img : JSON.stringify(img_obj),
|
||||
file: fileName,
|
||||
tempfile: file.uniqueIdentifier
|
||||
},
|
||||
success: function(){
|
||||
notifyMessage("Image uploaded correctly");
|
||||
$('div[id="'+fileName+'progressBar"]').remove();
|
||||
Sunstone.runAction("Image.refresh");
|
||||
},
|
||||
error: function(response){
|
||||
//onError({}, JSON.parse(response) );
|
||||
notifyMessage(response);
|
||||
$('div[id="'+fileName+'progressBar"]').remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#create_image',dialog).submit(function(){
|
||||
$create_image_dialog = dialog;
|
||||
@ -1063,7 +1070,8 @@ function initialize_create_image_dialog(dialog) {
|
||||
dialog.empty();
|
||||
setupCreateImageDialog();
|
||||
|
||||
uploader._onInputChange(file_input);
|
||||
//uploader._onInputChange(file_input);
|
||||
uploader.upload();
|
||||
} else {
|
||||
Sunstone.runAction("Image.create", img_obj);
|
||||
};
|
||||
|
20
src/sunstone/public/vendor/resumable/MIT-LICENSE
vendored
Normal file
20
src/sunstone/public/vendor/resumable/MIT-LICENSE
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2011, 23, http://www.23developer.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
817
src/sunstone/public/vendor/resumable/resumable.js
vendored
Normal file
817
src/sunstone/public/vendor/resumable/resumable.js
vendored
Normal file
@ -0,0 +1,817 @@
|
||||
/*
|
||||
* MIT Licensed
|
||||
* http://www.23developer.com/opensource
|
||||
* http://github.com/23/resumable.js
|
||||
* Steffen Tiedemann Christensen, steffen@23company.com
|
||||
*/
|
||||
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
var Resumable = function(opts){
|
||||
if ( !(this instanceof Resumable) ) {
|
||||
return new Resumable(opts);
|
||||
}
|
||||
this.version = 1.0;
|
||||
// SUPPORTED BY BROWSER?
|
||||
// Check if these features are support by the browser:
|
||||
// - File object type
|
||||
// - Blob object type
|
||||
// - FileList object type
|
||||
// - slicing files
|
||||
this.support = (
|
||||
(typeof(File)!=='undefined')
|
||||
&&
|
||||
(typeof(Blob)!=='undefined')
|
||||
&&
|
||||
(typeof(FileList)!=='undefined')
|
||||
&&
|
||||
(!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false)
|
||||
);
|
||||
if(!this.support) return(false);
|
||||
|
||||
|
||||
// PROPERTIES
|
||||
var $ = this;
|
||||
$.files = [];
|
||||
$.defaults = {
|
||||
chunkSize:1*1024*1024,
|
||||
forceChunkSize:false,
|
||||
simultaneousUploads:3,
|
||||
fileParameterName:'file',
|
||||
throttleProgressCallbacks:0.5,
|
||||
query:{},
|
||||
headers:{},
|
||||
preprocess:null,
|
||||
method:'multipart',
|
||||
prioritizeFirstAndLastChunk:false,
|
||||
target:'/',
|
||||
testChunks:true,
|
||||
generateUniqueIdentifier:null,
|
||||
maxChunkRetries:undefined,
|
||||
chunkRetryInterval:undefined,
|
||||
permanentErrors:[404, 415, 500, 501],
|
||||
maxFiles:undefined,
|
||||
withCredentials:false,
|
||||
xhrTimeout:0,
|
||||
maxFilesErrorCallback:function (files, errorCount) {
|
||||
var maxFiles = $.getOpt('maxFiles');
|
||||
alert('Please upload ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.');
|
||||
},
|
||||
minFileSize:1,
|
||||
minFileSizeErrorCallback:function(file, errorCount) {
|
||||
alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.');
|
||||
},
|
||||
maxFileSize:undefined,
|
||||
maxFileSizeErrorCallback:function(file, errorCount) {
|
||||
alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.');
|
||||
},
|
||||
fileType: [],
|
||||
fileTypeErrorCallback: function(file, errorCount) {
|
||||
alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.');
|
||||
}
|
||||
};
|
||||
$.opts = opts||{};
|
||||
$.getOpt = function(o) {
|
||||
var $opt = this;
|
||||
// Get multiple option if passed an array
|
||||
if(o instanceof Array) {
|
||||
var options = {};
|
||||
$h.each(o, function(option){
|
||||
options[option] = $opt.getOpt(option);
|
||||
});
|
||||
return options;
|
||||
}
|
||||
// Otherwise, just return a simple option
|
||||
if ($opt instanceof ResumableChunk) {
|
||||
if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
|
||||
else { $opt = $opt.fileObj; }
|
||||
}
|
||||
if ($opt instanceof ResumableFile) {
|
||||
if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
|
||||
else { $opt = $opt.resumableObj; }
|
||||
}
|
||||
if ($opt instanceof Resumable) {
|
||||
if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
|
||||
else { return $opt.defaults[o]; }
|
||||
}
|
||||
};
|
||||
|
||||
// EVENTS
|
||||
// catchAll(event, ...)
|
||||
// fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message),
|
||||
// complete(), progress(), error(message, file), pause()
|
||||
$.events = [];
|
||||
$.on = function(event,callback){
|
||||
$.events.push(event.toLowerCase(), callback);
|
||||
};
|
||||
$.fire = function(){
|
||||
// `arguments` is an object, not array, in FF, so:
|
||||
var args = [];
|
||||
for (var i=0; i<arguments.length; i++) args.push(arguments[i]);
|
||||
// Find event listeners, and support pseudo-event `catchAll`
|
||||
var event = args[0].toLowerCase();
|
||||
for (var i=0; i<=$.events.length; i+=2) {
|
||||
if($.events[i]==event) $.events[i+1].apply($,args.slice(1));
|
||||
if($.events[i]=='catchall') $.events[i+1].apply(null,args);
|
||||
}
|
||||
if(event=='fileerror') $.fire('error', args[2], args[1]);
|
||||
if(event=='fileprogress') $.fire('progress');
|
||||
};
|
||||
|
||||
|
||||
// INTERNAL HELPER METHODS (handy, but ultimately not part of uploading)
|
||||
var $h = {
|
||||
stopEvent: function(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
each: function(o,callback){
|
||||
if(typeof(o.length)!=='undefined') {
|
||||
for (var i=0; i<o.length; i++) {
|
||||
// Array or FileList
|
||||
if(callback(o[i])===false) return;
|
||||
}
|
||||
} else {
|
||||
for (i in o) {
|
||||
// Object
|
||||
if(callback(i,o[i])===false) return;
|
||||
}
|
||||
}
|
||||
},
|
||||
generateUniqueIdentifier:function(file){
|
||||
var custom = $.getOpt('generateUniqueIdentifier');
|
||||
if(typeof custom === 'function') {
|
||||
return custom(file);
|
||||
}
|
||||
var relativePath = file.webkitRelativePath||file.fileName||file.name; // Some confusion in different versions of Firefox
|
||||
var size = file.size;
|
||||
return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''));
|
||||
},
|
||||
contains:function(array,test) {
|
||||
var result = false;
|
||||
|
||||
$h.each(array, function(value) {
|
||||
if (value == test) {
|
||||
result = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
formatSize:function(size){
|
||||
if(size<1024) {
|
||||
return size + ' bytes';
|
||||
} else if(size<1024*1024) {
|
||||
return (size/1024.0).toFixed(0) + ' KB';
|
||||
} else if(size<1024*1024*1024) {
|
||||
return (size/1024.0/1024.0).toFixed(1) + ' MB';
|
||||
} else {
|
||||
return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB';
|
||||
}
|
||||
},
|
||||
getTarget:function(params){
|
||||
var target = $.getOpt('target');
|
||||
if(target.indexOf('?') < 0) {
|
||||
target += '?';
|
||||
} else {
|
||||
target += '&';
|
||||
}
|
||||
return target + params.join('&');
|
||||
}
|
||||
};
|
||||
|
||||
var onDrop = function(event){
|
||||
$h.stopEvent(event);
|
||||
appendFilesFromFileList(event.dataTransfer.files, event);
|
||||
};
|
||||
var onDragOver = function(e) {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// INTERNAL METHODS (both handy and responsible for the heavy load)
|
||||
var appendFilesFromFileList = function(fileList, event){
|
||||
// check for uploading too many files
|
||||
var errorCount = 0;
|
||||
var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']);
|
||||
if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) {
|
||||
// if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file
|
||||
if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) {
|
||||
$.removeFile($.files[0]);
|
||||
} else {
|
||||
o.maxFilesErrorCallback(fileList, errorCount++);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var files = [];
|
||||
$h.each(fileList, function(file){
|
||||
var fileName = file.name.split('.');
|
||||
var fileType = fileName[fileName.length-1].toLowerCase();
|
||||
|
||||
if (o.fileType.length > 0 && !$h.contains(o.fileType, fileType)) {
|
||||
o.fileTypeErrorCallback(file, errorCount++);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) {
|
||||
o.minFileSizeErrorCallback(file, errorCount++);
|
||||
return false;
|
||||
}
|
||||
if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) {
|
||||
o.maxFileSizeErrorCallback(file, errorCount++);
|
||||
return false;
|
||||
}
|
||||
|
||||
// directories have size == 0
|
||||
if (!$.getFromUniqueIdentifier($h.generateUniqueIdentifier(file))) {(function(){
|
||||
var f = new ResumableFile($, file);
|
||||
window.setTimeout(function(){
|
||||
$.files.push(f);
|
||||
files.push(f);
|
||||
f.container = (typeof event != 'undefined' ? event.srcElement : null);
|
||||
$.fire('fileAdded', f, event)
|
||||
},0);
|
||||
})()};
|
||||
});
|
||||
window.setTimeout(function(){
|
||||
$.fire('filesAdded', files)
|
||||
},0);
|
||||
};
|
||||
|
||||
// INTERNAL OBJECT TYPES
|
||||
function ResumableFile(resumableObj, file){
|
||||
var $ = this;
|
||||
$.opts = {};
|
||||
$.getOpt = resumableObj.getOpt;
|
||||
$._prevProgress = 0;
|
||||
$.resumableObj = resumableObj;
|
||||
$.file = file;
|
||||
$.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox
|
||||
$.size = file.size;
|
||||
$.relativePath = file.webkitRelativePath || $.fileName;
|
||||
$.uniqueIdentifier = $h.generateUniqueIdentifier(file);
|
||||
$._pause = false;
|
||||
$.container = '';
|
||||
var _error = false;
|
||||
|
||||
// Callback when something happens within the chunk
|
||||
var chunkEvent = function(event, message){
|
||||
// event can be 'progress', 'success', 'error' or 'retry'
|
||||
switch(event){
|
||||
case 'progress':
|
||||
$.resumableObj.fire('fileProgress', $);
|
||||
break;
|
||||
case 'error':
|
||||
$.abort();
|
||||
_error = true;
|
||||
$.chunks = [];
|
||||
$.resumableObj.fire('fileError', $, message);
|
||||
break;
|
||||
case 'success':
|
||||
if(_error) return;
|
||||
$.resumableObj.fire('fileProgress', $); // it's at least progress
|
||||
if($.isComplete()) {
|
||||
$.resumableObj.fire('fileSuccess', $, message);
|
||||
}
|
||||
break;
|
||||
case 'retry':
|
||||
$.resumableObj.fire('fileRetry', $);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Main code to set up a file object with chunks,
|
||||
// packaged to be able to handle retries if needed.
|
||||
$.chunks = [];
|
||||
$.abort = function(){
|
||||
// Stop current uploads
|
||||
var abortCount = 0;
|
||||
$h.each($.chunks, function(c){
|
||||
if(c.status()=='uploading') {
|
||||
c.abort();
|
||||
abortCount++;
|
||||
}
|
||||
});
|
||||
if(abortCount>0) $.resumableObj.fire('fileProgress', $);
|
||||
};
|
||||
$.cancel = function(){
|
||||
// Reset this file to be void
|
||||
var _chunks = $.chunks;
|
||||
$.chunks = [];
|
||||
// Stop current uploads
|
||||
$h.each(_chunks, function(c){
|
||||
if(c.status()=='uploading') {
|
||||
c.abort();
|
||||
$.resumableObj.uploadNextChunk();
|
||||
}
|
||||
});
|
||||
$.resumableObj.removeFile($);
|
||||
$.resumableObj.fire('fileProgress', $);
|
||||
};
|
||||
$.retry = function(){
|
||||
$.bootstrap();
|
||||
var firedRetry = false;
|
||||
$.resumableObj.on('chunkingComplete', function(){
|
||||
if(!firedRetry) $.resumableObj.upload();
|
||||
firedRetry = true;
|
||||
});
|
||||
};
|
||||
$.bootstrap = function(){
|
||||
$.abort();
|
||||
_error = false;
|
||||
// Rebuild stack of chunks from file
|
||||
$.chunks = [];
|
||||
$._prevProgress = 0;
|
||||
var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor;
|
||||
var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1);
|
||||
for (var offset=0; offset<maxOffset; offset++) {(function(offset){
|
||||
window.setTimeout(function(){
|
||||
$.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent));
|
||||
$.resumableObj.fire('chunkingProgress',$,offset/maxOffset);
|
||||
},0);
|
||||
})(offset)}
|
||||
window.setTimeout(function(){
|
||||
$.resumableObj.fire('chunkingComplete',$);
|
||||
},0);
|
||||
};
|
||||
$.progress = function(){
|
||||
if(_error) return(1);
|
||||
// Sum up progress across everything
|
||||
var ret = 0;
|
||||
var error = false;
|
||||
$h.each($.chunks, function(c){
|
||||
if(c.status()=='error') error = true;
|
||||
ret += c.progress(true); // get chunk progress relative to entire file
|
||||
});
|
||||
ret = (error ? 1 : (ret>0.999 ? 1 : ret));
|
||||
ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused
|
||||
$._prevProgress = ret;
|
||||
return(ret);
|
||||
};
|
||||
$.isUploading = function(){
|
||||
var uploading = false;
|
||||
$h.each($.chunks, function(chunk){
|
||||
if(chunk.status()=='uploading') {
|
||||
uploading = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
return(uploading);
|
||||
};
|
||||
$.isComplete = function(){
|
||||
var outstanding = false;
|
||||
$h.each($.chunks, function(chunk){
|
||||
var status = chunk.status();
|
||||
if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) {
|
||||
outstanding = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
return(!outstanding);
|
||||
};
|
||||
$.pause = function(pause){
|
||||
if(typeof(pause)==='undefined'){
|
||||
$._pause = ($._pause ? false : true);
|
||||
}else{
|
||||
$._pause = pause;
|
||||
}
|
||||
};
|
||||
$.isPaused = function() {
|
||||
return $._pause;
|
||||
};
|
||||
|
||||
|
||||
// Bootstrap and return
|
||||
$.resumableObj.fire('chunkingStart', $);
|
||||
$.bootstrap();
|
||||
return(this);
|
||||
}
|
||||
|
||||
function ResumableChunk(resumableObj, fileObj, offset, callback){
|
||||
var $ = this;
|
||||
$.opts = {};
|
||||
$.getOpt = resumableObj.getOpt;
|
||||
$.resumableObj = resumableObj;
|
||||
$.fileObj = fileObj;
|
||||
$.fileObjSize = fileObj.size;
|
||||
$.fileObjType = fileObj.file.type;
|
||||
$.offset = offset;
|
||||
$.callback = callback;
|
||||
$.lastProgressCallback = (new Date);
|
||||
$.tested = false;
|
||||
$.retries = 0;
|
||||
$.pendingRetry = false;
|
||||
$.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
|
||||
|
||||
// Computed properties
|
||||
var chunkSize = $.getOpt('chunkSize');
|
||||
$.loaded = 0;
|
||||
$.startByte = $.offset*chunkSize;
|
||||
$.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize);
|
||||
if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) {
|
||||
// The last chunk will be bigger than the chunk size, but less than 2*chunkSize
|
||||
$.endByte = $.fileObjSize;
|
||||
}
|
||||
$.xhr = null;
|
||||
|
||||
// test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session
|
||||
$.test = function(){
|
||||
// Set up request and listen for event
|
||||
$.xhr = new XMLHttpRequest();
|
||||
|
||||
var testHandler = function(e){
|
||||
$.tested = true;
|
||||
var status = $.status();
|
||||
if(status=='success') {
|
||||
$.callback(status, $.message());
|
||||
$.resumableObj.uploadNextChunk();
|
||||
} else {
|
||||
$.send();
|
||||
}
|
||||
};
|
||||
$.xhr.addEventListener('load', testHandler, false);
|
||||
$.xhr.addEventListener('error', testHandler, false);
|
||||
|
||||
// Add data from the query options
|
||||
var params = [];
|
||||
var customQuery = $.getOpt('query');
|
||||
if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
|
||||
$h.each(customQuery, function(k,v){
|
||||
params.push([encodeURIComponent(k), encodeURIComponent(v)].join('='));
|
||||
});
|
||||
// Add extra data to identify chunk
|
||||
params.push(['resumableChunkNumber', encodeURIComponent($.offset+1)].join('='));
|
||||
params.push(['resumableChunkSize', encodeURIComponent($.getOpt('chunkSize'))].join('='));
|
||||
params.push(['resumableCurrentChunkSize', encodeURIComponent($.endByte - $.startByte)].join('='));
|
||||
params.push(['resumableTotalSize', encodeURIComponent($.fileObjSize)].join('='));
|
||||
params.push(['resumableType', encodeURIComponent($.fileObjType)].join('='));
|
||||
params.push(['resumableIdentifier', encodeURIComponent($.fileObj.uniqueIdentifier)].join('='));
|
||||
params.push(['resumableFilename', encodeURIComponent($.fileObj.fileName)].join('='));
|
||||
params.push(['resumableRelativePath', encodeURIComponent($.fileObj.relativePath)].join('='));
|
||||
params.push(['resumableTotalChunks', encodeURIComponent($.fileObj.chunks.length)].join('='));
|
||||
// Append the relevant chunk and send it
|
||||
$.xhr.open('GET', $h.getTarget(params));
|
||||
$.xhr.timeout = $.getOpt('xhrTimeout');
|
||||
$.xhr.withCredentials = $.getOpt('withCredentials');
|
||||
// Add data from header options
|
||||
$h.each($.getOpt('headers'), function(k,v) {
|
||||
$.xhr.setRequestHeader(k, v);
|
||||
});
|
||||
$.xhr.send(null);
|
||||
};
|
||||
|
||||
$.preprocessFinished = function(){
|
||||
$.preprocessState = 2;
|
||||
$.send();
|
||||
};
|
||||
|
||||
// send() uploads the actual data in a POST call
|
||||
$.send = function(){
|
||||
var preprocess = $.getOpt('preprocess');
|
||||
if(typeof preprocess === 'function') {
|
||||
switch($.preprocessState) {
|
||||
case 0: preprocess($); $.preprocessState = 1; return;
|
||||
case 1: return;
|
||||
case 2: break;
|
||||
}
|
||||
}
|
||||
if($.getOpt('testChunks') && !$.tested) {
|
||||
$.test();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up request and listen for event
|
||||
$.xhr = new XMLHttpRequest();
|
||||
|
||||
// Progress
|
||||
$.xhr.upload.addEventListener('progress', function(e){
|
||||
if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) {
|
||||
$.callback('progress');
|
||||
$.lastProgressCallback = (new Date);
|
||||
}
|
||||
$.loaded=e.loaded||0;
|
||||
}, false);
|
||||
$.loaded = 0;
|
||||
$.pendingRetry = false;
|
||||
$.callback('progress');
|
||||
|
||||
// Done (either done, failed or retry)
|
||||
var doneHandler = function(e){
|
||||
var status = $.status();
|
||||
if(status=='success'||status=='error') {
|
||||
$.callback(status, $.message());
|
||||
$.resumableObj.uploadNextChunk();
|
||||
} else {
|
||||
$.callback('retry', $.message());
|
||||
$.abort();
|
||||
$.retries++;
|
||||
var retryInterval = $.getOpt('chunkRetryInterval');
|
||||
if(retryInterval !== undefined) {
|
||||
$.pendingRetry = true;
|
||||
setTimeout($.send, retryInterval);
|
||||
} else {
|
||||
$.send();
|
||||
}
|
||||
}
|
||||
};
|
||||
$.xhr.addEventListener('load', doneHandler, false);
|
||||
$.xhr.addEventListener('error', doneHandler, false);
|
||||
|
||||
// Set up the basic query data from Resumable
|
||||
var query = {
|
||||
resumableChunkNumber: $.offset+1,
|
||||
resumableChunkSize: $.getOpt('chunkSize'),
|
||||
resumableCurrentChunkSize: $.endByte - $.startByte,
|
||||
resumableTotalSize: $.fileObjSize,
|
||||
resumableType: $.fileObjType,
|
||||
resumableIdentifier: $.fileObj.uniqueIdentifier,
|
||||
resumableFilename: $.fileObj.fileName,
|
||||
resumableRelativePath: $.fileObj.relativePath,
|
||||
resumableTotalChunks: $.fileObj.chunks.length
|
||||
};
|
||||
// Mix in custom data
|
||||
var customQuery = $.getOpt('query');
|
||||
if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
|
||||
$h.each(customQuery, function(k,v){
|
||||
query[k] = v;
|
||||
});
|
||||
|
||||
var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))),
|
||||
bytes = $.fileObj.file[func]($.startByte,$.endByte),
|
||||
data = null,
|
||||
target = $.getOpt('target');
|
||||
|
||||
if ($.getOpt('method') === 'octet') {
|
||||
// Add data from the query options
|
||||
data = bytes;
|
||||
var params = [];
|
||||
$h.each(query, function(k,v){
|
||||
params.push([encodeURIComponent(k), encodeURIComponent(v)].join('='));
|
||||
});
|
||||
target = $h.getTarget(params);
|
||||
} else {
|
||||
// Add data from the query options
|
||||
data = new FormData();
|
||||
$h.each(query, function(k,v){
|
||||
data.append(k,v);
|
||||
});
|
||||
data.append($.getOpt('fileParameterName'), bytes);
|
||||
}
|
||||
|
||||
$.xhr.open('POST', target);
|
||||
$.xhr.timeout = $.getOpt('xhrTimeout');
|
||||
$.xhr.withCredentials = $.getOpt('withCredentials');
|
||||
// Add data from header options
|
||||
$h.each($.getOpt('headers'), function(k,v) {
|
||||
$.xhr.setRequestHeader(k, v);
|
||||
});
|
||||
$.xhr.send(data);
|
||||
};
|
||||
$.abort = function(){
|
||||
// Abort and reset
|
||||
if($.xhr) $.xhr.abort();
|
||||
$.xhr = null;
|
||||
};
|
||||
$.status = function(){
|
||||
// Returns: 'pending', 'uploading', 'success', 'error'
|
||||
if($.pendingRetry) {
|
||||
// if pending retry then that's effectively the same as actively uploading,
|
||||
// there might just be a slight delay before the retry starts
|
||||
return('uploading');
|
||||
} else if(!$.xhr) {
|
||||
return('pending');
|
||||
} else if($.xhr.readyState<4) {
|
||||
// Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening
|
||||
return('uploading');
|
||||
} else {
|
||||
if($.xhr.status==200) {
|
||||
// HTTP 200, perfect
|
||||
return('success');
|
||||
} else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) {
|
||||
// HTTP 415/500/501, permanent error
|
||||
return('error');
|
||||
} else {
|
||||
// this should never happen, but we'll reset and queue a retry
|
||||
// a likely case for this would be 503 service unavailable
|
||||
$.abort();
|
||||
return('pending');
|
||||
}
|
||||
}
|
||||
};
|
||||
$.message = function(){
|
||||
return($.xhr ? $.xhr.responseText : '');
|
||||
};
|
||||
$.progress = function(relative){
|
||||
if(typeof(relative)==='undefined') relative = false;
|
||||
var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1);
|
||||
if($.pendingRetry) return(0);
|
||||
var s = $.status();
|
||||
switch(s){
|
||||
case 'success':
|
||||
case 'error':
|
||||
return(1*factor);
|
||||
case 'pending':
|
||||
return(0*factor);
|
||||
default:
|
||||
return($.loaded/($.endByte-$.startByte)*factor);
|
||||
}
|
||||
};
|
||||
return(this);
|
||||
}
|
||||
|
||||
// QUEUE
|
||||
$.uploadNextChunk = function(){
|
||||
var found = false;
|
||||
|
||||
// In some cases (such as videos) it's really handy to upload the first
|
||||
// and last chunk of a file quickly; this let's the server check the file's
|
||||
// metadata and determine if there's even a point in continuing.
|
||||
if ($.getOpt('prioritizeFirstAndLastChunk')) {
|
||||
$h.each($.files, function(file){
|
||||
if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) {
|
||||
file.chunks[0].send();
|
||||
found = true;
|
||||
return(false);
|
||||
}
|
||||
if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) {
|
||||
file.chunks[file.chunks.length-1].send();
|
||||
found = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
if(found) return(true);
|
||||
}
|
||||
|
||||
// Now, simply look for the next, best thing to upload
|
||||
$h.each($.files, function(file){
|
||||
if(file.isPaused()===false){
|
||||
$h.each(file.chunks, function(chunk){
|
||||
if(chunk.status()=='pending' && chunk.preprocessState === 0) {
|
||||
chunk.send();
|
||||
found = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
if(found) return(false);
|
||||
});
|
||||
if(found) return(true);
|
||||
|
||||
// The are no more outstanding chunks to upload, check is everything is done
|
||||
var outstanding = false;
|
||||
$h.each($.files, function(file){
|
||||
if(!file.isComplete()) {
|
||||
outstanding = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
if(!outstanding) {
|
||||
// All chunks have been uploaded, complete
|
||||
$.fire('complete');
|
||||
}
|
||||
return(false);
|
||||
};
|
||||
|
||||
|
||||
// PUBLIC METHODS FOR RESUMABLE.JS
|
||||
$.assignBrowse = function(domNodes, isDirectory){
|
||||
if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
|
||||
|
||||
$h.each(domNodes, function(domNode) {
|
||||
var input;
|
||||
if(domNode.tagName==='INPUT' && domNode.type==='file'){
|
||||
input = domNode;
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.style.display = 'none';
|
||||
domNode.addEventListener('click', function(){
|
||||
input.style.opacity = 0;
|
||||
input.style.display='block';
|
||||
input.focus();
|
||||
input.click();
|
||||
input.style.display='none';
|
||||
}, false);
|
||||
domNode.appendChild(input);
|
||||
}
|
||||
var maxFiles = $.getOpt('maxFiles');
|
||||
if (typeof(maxFiles)==='undefined'||maxFiles!=1){
|
||||
input.setAttribute('multiple', 'multiple');
|
||||
} else {
|
||||
input.removeAttribute('multiple');
|
||||
}
|
||||
if(isDirectory){
|
||||
input.setAttribute('webkitdirectory', 'webkitdirectory');
|
||||
} else {
|
||||
input.removeAttribute('webkitdirectory');
|
||||
}
|
||||
// When new files are added, simply append them to the overall list
|
||||
input.addEventListener('change', function(e){
|
||||
appendFilesFromFileList(e.target.files,e);
|
||||
e.target.value = '';
|
||||
}, false);
|
||||
});
|
||||
};
|
||||
$.assignDrop = function(domNodes){
|
||||
if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
|
||||
|
||||
$h.each(domNodes, function(domNode) {
|
||||
domNode.addEventListener('dragover', onDragOver, false);
|
||||
domNode.addEventListener('drop', onDrop, false);
|
||||
});
|
||||
};
|
||||
$.unAssignDrop = function(domNodes) {
|
||||
if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes];
|
||||
|
||||
$h.each(domNodes, function(domNode) {
|
||||
domNode.removeEventListener('dragover', onDragOver);
|
||||
domNode.removeEventListener('drop', onDrop);
|
||||
});
|
||||
};
|
||||
$.isUploading = function(){
|
||||
var uploading = false;
|
||||
$h.each($.files, function(file){
|
||||
if (file.isUploading()) {
|
||||
uploading = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
return(uploading);
|
||||
};
|
||||
$.upload = function(){
|
||||
// Make sure we don't start too many uploads at once
|
||||
if($.isUploading()) return;
|
||||
// Kick off the queue
|
||||
$.fire('uploadStart');
|
||||
for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) {
|
||||
$.uploadNextChunk();
|
||||
}
|
||||
};
|
||||
$.pause = function(){
|
||||
// Resume all chunks currently being uploaded
|
||||
$h.each($.files, function(file){
|
||||
file.abort();
|
||||
});
|
||||
$.fire('pause');
|
||||
};
|
||||
$.cancel = function(){
|
||||
for(var i = $.files.length - 1; i >= 0; i--) {
|
||||
$.files[i].cancel();
|
||||
}
|
||||
$.fire('cancel');
|
||||
};
|
||||
$.progress = function(){
|
||||
var totalDone = 0;
|
||||
var totalSize = 0;
|
||||
// Resume all chunks currently being uploaded
|
||||
$h.each($.files, function(file){
|
||||
totalDone += file.progress()*file.size;
|
||||
totalSize += file.size;
|
||||
});
|
||||
return(totalSize>0 ? totalDone/totalSize : 0);
|
||||
};
|
||||
$.addFile = function(file, event){
|
||||
appendFilesFromFileList([file], event);
|
||||
};
|
||||
$.removeFile = function(file){
|
||||
for(var i = $.files.length - 1; i >= 0; i--) {
|
||||
if($.files[i] === file) {
|
||||
$.files.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
$.getFromUniqueIdentifier = function(uniqueIdentifier){
|
||||
var ret = false;
|
||||
$h.each($.files, function(f){
|
||||
if(f.uniqueIdentifier==uniqueIdentifier) ret = f;
|
||||
});
|
||||
return(ret);
|
||||
};
|
||||
$.getSize = function(){
|
||||
var totalSize = 0;
|
||||
$h.each($.files, function(file){
|
||||
totalSize += file.size;
|
||||
});
|
||||
return(totalSize);
|
||||
};
|
||||
|
||||
return(this);
|
||||
};
|
||||
|
||||
|
||||
// Node.js-style export for Node and Component
|
||||
if (typeof module != 'undefined') {
|
||||
module.exports = Resumable;
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
// AMD/requirejs: Define the module
|
||||
define(function(){
|
||||
return Resumable;
|
||||
});
|
||||
} else {
|
||||
// Browser: Expose to window
|
||||
window.Resumable = Resumable;
|
||||
}
|
||||
|
||||
})();
|
@ -58,6 +58,8 @@ require 'sinatra'
|
||||
require 'erb'
|
||||
require 'yaml'
|
||||
require 'securerandom'
|
||||
require 'tmpdir'
|
||||
require 'fileutils'
|
||||
|
||||
require 'CloudAuth'
|
||||
require 'SunstoneServer'
|
||||
@ -500,27 +502,63 @@ end
|
||||
##############################################################################
|
||||
post '/upload' do
|
||||
tmpfile = nil
|
||||
rackinput = request.env['rack.input']
|
||||
|
||||
if (rackinput.class == Tempfile)
|
||||
tmpfile = rackinput
|
||||
elsif rackinput.respond_to?('read')
|
||||
tmpfile = Tempfile.open('sunstone-upload')
|
||||
tmpfile.write(rackinput.read)
|
||||
tmpfile.flush
|
||||
else
|
||||
logger.error { "Unexpected rackinput class #{rackinput.class}" }
|
||||
[500, ""]
|
||||
end
|
||||
name = params[:tempfile]
|
||||
|
||||
if tmpfile.size == 0
|
||||
if !name
|
||||
[500, OpenNebula::Error.new("There was a problem uploading the file, " \
|
||||
"please check the permissions on the file").to_json]
|
||||
else
|
||||
@SunstoneServer.upload(params[:img], tmpfile.path)
|
||||
tmpfile = File.join(Dir.tmpdir, name)
|
||||
res = @SunstoneServer.upload(params[:img], tmpfile)
|
||||
FileUtils.rm(tmpfile)
|
||||
res
|
||||
end
|
||||
end
|
||||
|
||||
post '/upload_chunk' do
|
||||
info = env['rack.request.form_hash']
|
||||
chunk_number = info['resumableChunkNumber'].to_i - 1
|
||||
chunk_size = info['resumableChunkSize'].to_i
|
||||
chunk_current_size = info['resumableCurrentChunkSize'].to_i
|
||||
chunk_start = chunk_number * chunk_size
|
||||
chunk_end = chunk_start + chunk_current_size - 1
|
||||
identifier = info['']
|
||||
size = info['resumableTotalSize'].to_i
|
||||
|
||||
file_name = info['resumableIdentifier']
|
||||
file_path = File.join(Dir.tmpdir, file_name)
|
||||
|
||||
tmpfile=env['rack.request.form_hash']['file'][:tempfile]
|
||||
|
||||
begin
|
||||
chunk = tmpfile.read
|
||||
rescue => e
|
||||
STDERR.puts e.backtrace
|
||||
return [500, OpenNebula::Error.new("Could not read the uploaded " \
|
||||
"chunk.".to_json)]
|
||||
end
|
||||
|
||||
if File.exist? file_name
|
||||
mode = "r+"
|
||||
else
|
||||
mode = "w"
|
||||
end
|
||||
|
||||
begin
|
||||
open(file_path, mode) do |f|
|
||||
f.seek(chunk_start)
|
||||
f.write_nonblock(chunk)
|
||||
end
|
||||
rescue => e
|
||||
STDERR.puts e.backtrace
|
||||
return [500, OpenNebula::Error.new("Can not write to the temporary" \
|
||||
" image file").to_json]
|
||||
end
|
||||
|
||||
""
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
# Create a new Resource
|
||||
##############################################################################
|
||||
|
@ -35,6 +35,7 @@
|
||||
<script src="vendor/noVNC/util.js"></script>
|
||||
|
||||
<script src="vendor/fileuploader/fileuploader.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
|
||||
<script src="vendor/resumable/resumable.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
|
||||
<script src="vendor/4.0/nouislider/jquery.nouislider.min.js?v=<%= OpenNebula::VERSION %>" type="text/javascript"></script>
|
||||
<link href="vendor/4.0/nouislider/nouislider.css?v=<%= OpenNebula::VERSION %>" rel="stylesheet" type="text/css"></link>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user