
Upload ISO to the path /var/lib/kimchi/iso of the local disk Signed-off-by: ssdxiao <ssdxiao@163.com> --- contrib/kimchi.spec.fedora.in | 1 + contrib/kimchi.spec.suse.in | 1 + po/en_US.po | 3 + po/pt_BR.po | 3 + po/zh_CN.po | 3 + src/kimchi/control/storagepools.py | 28 +- src/nginx.conf.in | 1 + ui/css/theme-default/upload.css | 43 ++ ui/js/resumable.js | 816 +++++++++++++++++++++++++++++++++ ui/js/src/kimchi.template_add_main.js | 27 ++ ui/pages/kimchi-ui.html.tmpl | 1 + ui/pages/template-add.html.tmpl | 13 + 12 files changed, 938 insertions(+), 2 deletions(-) create mode 100644 ui/css/theme-default/upload.css create mode 100644 ui/js/resumable.js diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 2d4699b..771fccc 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -164,6 +164,7 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/kimchi/ui/js/kimchi.min.js %{_datadir}/kimchi/ui/js/jquery-ui.js %{_datadir}/kimchi/ui/js/jquery.min.js +%{_datadir}/kimchi/ui/js/resumable.js %{_datadir}/kimchi/ui/js/modernizr.custom.2.6.2.min.js %{_datadir}/kimchi/ui/js/novnc/*.js %{_datadir}/kimchi/ui/js/spice/*.js diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 165f566..ad6aed4 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -86,6 +86,7 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/kimchi/ui/js/kimchi.min.js %{_datadir}/kimchi/ui/js/jquery-ui.js %{_datadir}/kimchi/ui/js/jquery.min.js +%{_datadir}/kimchi/ui/js/resumable.js %{_datadir}/kimchi/ui/js/modernizr.custom.2.6.2.min.js %{_datadir}/kimchi/ui/js/novnc/*.js %{_datadir}/kimchi/ui/js/spice/*.js diff --git a/po/en_US.po b/po/en_US.po index 1ede7dc..6f5b100 100644 --- a/po/en_US.po +++ b/po/en_US.po @@ -1670,3 +1670,6 @@ msgstr "No templates found." msgid "Clone" msgstr "" + +msgid "Upload ISO Image" +msgstr "Upload ISO Image" diff --git a/po/pt_BR.po b/po/pt_BR.po index 5ff54e0..d4d26ee 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1777,3 +1777,6 @@ msgstr "Nenhum modelo encontrado." msgid "Clone" msgstr "" + +msgid "Upload ISO Image" +msgstr "Carregar Imagem ISO" diff --git a/po/zh_CN.po b/po/zh_CN.po index caef515..da62131 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -1679,3 +1679,6 @@ msgstr "没有发现模板" msgid "Clone" msgstr "" + +msgid "Upload ISO Image" +msgstr "上传ISO镜像" \ No newline at end of file diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index b75bca0..72b9f78 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -18,8 +18,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import cherrypy - - +import os +import errno from kimchi.control.base import Collection, Resource from kimchi.control.storagevolumes import IsoVolumes, StorageVolumes from kimchi.control.utils import get_class_name, model_fn @@ -28,6 +28,9 @@ from kimchi.model.storagepools import ISO_POOL_NAME from kimchi.control.utils import UrlSubNode +ISO_UPLOAD_DIR = "/var/lib/kimchi/iso/" + + @UrlSubNode("storagepools", True, ['POST', 'DELETE']) class StoragePools(Collection): def __init__(self, model): @@ -35,6 +38,11 @@ class StoragePools(Collection): self.resource = StoragePool isos = IsoPool(model) setattr(self, ISO_POOL_NAME, isos) + try: + os.makedirs(ISO_UPLOAD_DIR, mode=0755) + except OSError as e: + if e.errno == errno.EEXIST: + pass def create(self, params, *args): try: @@ -57,6 +65,22 @@ class StoragePools(Collection): return resp + @cherrypy.expose + def upload(self, *args, **kwargs): + method = cherrypy.request.method.upper() + if method != "POST": + raise cherrypy.HTTPError(405) + fileName = kwargs["resumableFilename"] + chunkSize = kwargs["resumableChunkSize"] + chunkNumber = kwargs["resumableChunkNumber"] + position = int(chunkSize) * (int(chunkNumber)-1) + + filePath = ISO_UPLOAD_DIR+fileName + fp = open(filePath, "a+") + fp.seek(position) + fp.write(kwargs["file"].fullvalue()) + fp.close() + def _get_resources(self, filter_params): try: res_list = super(StoragePools, self)._get_resources(filter_params) diff --git a/src/nginx.conf.in b/src/nginx.conf.in index 38e643d..9568476 100644 --- a/src/nginx.conf.in +++ b/src/nginx.conf.in @@ -37,6 +37,7 @@ http { access_log /var/log/nginx/access.log main; sendfile on; + client_max_body_size 2m; server { listen $proxy_ssl_port ssl; diff --git a/ui/css/theme-default/upload.css b/ui/css/theme-default/upload.css new file mode 100644 index 0000000..9cdfe4f --- /dev/null +++ b/ui/css/theme-default/upload.css @@ -0,0 +1,43 @@ +/* +Uploadify +Copyright (c) 2012 Reactive Apps, Ronnie Garcia +Released under the MIT License <http://www.opensource.org/licenses/mit-license.php> +*/ + +.uploadify-button { + background-color: #505050; + background-image: linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -o-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -moz-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -webkit-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -ms-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0, #505050), + color-stop(1, #707070) + ); + background-position: center top; + background-repeat: no-repeat; + -webkit-border-radius: 30px; + -moz-border-radius: 30px; + border-radius: 30px; + border: 2px solid #808080; + color: #FFF; + height: 30px; + width: 120px; + font: bold 12px Arial, Helvetica, sans-serif; + text-align: center; + text-shadow: 0 -1px 0 rgba(0,0,0,0.25); +} +.uploadify-progress { + background-color: #E5E5E5; + margin-top: 10px; + width: 100%; +} +.uploadify-progress-bar { + background-color: #0099FF; + height: 3px; + width: 1px; +} diff --git a/ui/js/resumable.js b/ui/js/resumable.js new file mode 100644 index 0000000..add21ec --- /dev/null +++ b/ui/js/resumable.js @@ -0,0 +1,816 @@ +/* +* 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('=')); + // 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/ui/js/src/kimchi.template_add_main.js b/ui/js/src/kimchi.template_add_main.js index dbb3952..5651424 100644 --- a/ui/js/src/kimchi.template_add_main.js +++ b/ui/js/src/kimchi.template_add_main.js @@ -387,6 +387,33 @@ kimchi.template_add_main = function() { } } }; + //1-3 upload iso + $('#iso-upload').click(function() { + kimchi.switchPage('iso-type-box', 'iso-upload-box'); + }); + + $('#iso-upload-box-back').click(function() { + kimchi.switchPage('iso-upload-box', 'iso-type-box', 'right'); + }); + + var r = new Resumable({ + target:'storagepools/upload' + }); + + r.on('fileProgress', function(file){ + console.debug(file); + var element=document.getElementById("upload"); + var progress = Math.round(file.progress()*100)+"%" + element.innerHTML=file.fileName+ "-" + progress; + var tmp=document.getElementById("movie"); + tmp.innerHTML=['<div class="uploadify-progress"><div class="uploadify-progress-bar" style="width:', progress,'"></div></div>'].join("") + }); + + r.on('fileAdded', function(file, event){ + r.upload(); + }); + + r.assignBrowse(document.getElementById('browseButton')); }; kimchi.template_check_url = function(url) { diff --git a/ui/pages/kimchi-ui.html.tmpl b/ui/pages/kimchi-ui.html.tmpl index 08b27a8..542cd43 100644 --- a/ui/pages/kimchi-ui.html.tmpl +++ b/ui/pages/kimchi-ui.html.tmpl @@ -38,6 +38,7 @@ <script src="$href('libs/jquery-ui.min.js')"></script> <script src="$href('libs/jquery-ui-i18n.min.js')"></script> <script src="$href('js/kimchi.min.js')"></script> +<script src="$href('js/resumable.js')"></script> <!-- This is used for detecting if the UI needs to be built --> <style type="text/css"> diff --git a/ui/pages/template-add.html.tmpl b/ui/pages/template-add.html.tmpl index afe22dd..ecda083 100644 --- a/ui/pages/template-add.html.tmpl +++ b/ui/pages/template-add.html.tmpl @@ -41,6 +41,9 @@ <li> <a id="iso-remote" class="remote">$_("Remote ISO Image")</a> </li> + <li> + <a id="iso-upload" class="local">$_("Upload ISO Image")</a> + </li> </ul> </div> @@ -204,6 +207,16 @@ </div> </div> + <!-- 1-3--> + <div class="page" id="iso-upload-box"> + <header> + <a class="back" id="iso-upload-box-back"></a> + <h2 class="step-title">$_("Upload ISO Image")</h2> + </header> + <a href="#" id="browseButton" class="uploadify-button">Select files</a> + <div id="upload"></div> + <div id="movie"></div> + </div> </div> </div> </div> -- 1.7.9.5