Thanks for your replay .
 I test my patch , I find nginx will proxy https to http, so my patch can work .
 Kimchi  listens to 127.0.0.1 , and not allow other to access , why we must support https using cherrypy? .
 I will see your patch and work on upload with you . 

At 2014-06-10 11:52:35, "Royce Lv" <lvroyce@linux.vnet.ibm.com> wrote: >On 2014年06月10日 10:56, ssdxiao wrote: >> 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) >Hi, > Thanks for your patches! As I'm working on another patch of >creating a dedicate storage pool, I suggest we create this dedicate pool >when kimchi start up, and only make dirs does not make it a storage >pool. we need to call libvirt API. If you are willing to help on upload, >could you pls rebase on my patch? Thanks! >> + 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): > >I have tried cherrypy upload: >https://bitbucket.org/cherrypy/cherrypy/issue/1068/file-upload-crashes-when-using-https >And found cherrypy on https upload has bug, and this bug seems not back >ported on ubuntu. Does cherrypy work for your code on https? > >> + 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> >