From 0e15631291140e6b8b308bf13eb0a35947ab7899 Mon Sep 17 00:00:00 2001 From: Javi Fontan Date: Fri, 19 Sep 2014 17:40:59 +0200 Subject: [PATCH] feature #3194: Change image upload to use resumable library Now images are uploaded in chunks and they are retried when an error happens --- src/sunstone/public/js/plugins/images-tab.js | 116 +-- .../public/vendor/resumable/MIT-LICENSE | 20 + .../public/vendor/resumable/resumable.js | 817 ++++++++++++++++++ src/sunstone/sunstone-server.rb | 64 +- src/sunstone/views/index.erb | 1 + 5 files changed, 951 insertions(+), 67 deletions(-) create mode 100644 src/sunstone/public/vendor/resumable/MIT-LICENSE create mode 100644 src/sunstone/public/vendor/resumable/resumable.js diff --git a/src/sunstone/public/js/plugins/images-tab.js b/src/sunstone/public/js/plugins/images-tab.js index 00ec7dcc76..61ede292d3 100644 --- a/src/sunstone/public/js/plugins/images-tab.js +++ b/src/sunstone/public/js/plugins/images-tab.js @@ -97,8 +97,9 @@ var create_image_tmpl ='
\
\ \
\ -
\ -
\ +
'+ + tr("Click to choose image")+ + '
\
\
\
\ @@ -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('
\ -
\ - '+tr("Uploading...")+'\ -
\ -
\ -
\ - \ -
\ -
'+fileName+'
\ -
\ -
'); - }, - 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('
\ +
\ + '+tr("Uploading...")+'\ +
\ +
\ +
\ + \ +
\ +
'+fileName+'
\ +
\ +
'); + }); + + 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); }; diff --git a/src/sunstone/public/vendor/resumable/MIT-LICENSE b/src/sunstone/public/vendor/resumable/MIT-LICENSE new file mode 100644 index 0000000000..9d1da89a08 --- /dev/null +++ b/src/sunstone/public/vendor/resumable/MIT-LICENSE @@ -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. \ No newline at end of file diff --git a/src/sunstone/public/vendor/resumable/resumable.js b/src/sunstone/public/vendor/resumable/resumable.js new file mode 100644 index 0000000000..43790307da --- /dev/null +++ b/src/sunstone/public/vendor/resumable/resumable.js @@ -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 0 && !$h.contains(o.fileType, fileType)) { + o.fileTypeErrorCallback(file, errorCount++); + return false; + } + + if (typeof(o.minFileSize)!=='undefined' && file.sizeo.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; offset0.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; + } + +})(); diff --git a/src/sunstone/sunstone-server.rb b/src/sunstone/sunstone-server.rb index b396523fbc..25608cb71a 100755 --- a/src/sunstone/sunstone-server.rb +++ b/src/sunstone/sunstone-server.rb @@ -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 ############################################################################## diff --git a/src/sunstone/views/index.erb b/src/sunstone/views/index.erb index 7434466331..8d4cb966f3 100644 --- a/src/sunstone/views/index.erb +++ b/src/sunstone/views/index.erb @@ -35,6 +35,7 @@ +