[Kimchi-devel] [PATCH] Support to upload ISO

ssdxiao ssdxiao at 163.com
Tue Jun 10 02:56:07 UTC 2014


Upload ISO to the path /var/lib/kimchi/iso of the local disk

Signed-off-by: ssdxiao <ssdxiao at 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 at 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





More information about the Kimchi-devel mailing list