[Kimchi-devel] [WIP][PATCH 4/4] Support to upload ISO

Aline Manera alinefm at linux.vnet.ibm.com
Thu Aug 21 20:02:39 UTC 2014


On 08/21/2014 11:43 AM, lvroyce0210 at gmail.com wrote:
> From: ssdxiao <ssdxiao at 163.com>
>
> Upload ISO to the path /var/lib/kimchi/iso of the local disk

Haven't we agreed to use upload/download in any pool?

>
> Signed-off-by: ssdxiao <ssdxiao at 163.com>
> Signed-off-by: Royce Lv <lvroyce at linux.vnet.ibm.com>
> ---
>   contrib/kimchi.spec.fedora.in         |   3 +
>   contrib/kimchi.spec.suse.in           |   3 +
>   po/en_US.po                           |   2 +
>   po/pt_BR.po                           |   3 +
>   po/zh_CN.po                           |   3 +
>   src/kimchi/API.json                   |   1 -
>   src/kimchi/control/storagepools.py    |   4 +-
>   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 +
>   13 files changed, 917 insertions(+), 3 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 5766784..6be0a26 100644
> --- a/contrib/kimchi.spec.fedora.in
> +++ b/contrib/kimchi.spec.fedora.in
> @@ -175,6 +175,9 @@ rm -rf $RPM_BUILD_ROOT
>   %{_datadir}/kimchi/ui/images/theme-default/*.png
>   %{_datadir}/kimchi/ui/images/theme-default/*.gif
>   %{_datadir}/kimchi/ui/js/kimchi.min.js

> +%{_datadir}/kimchi/ui/js/jquery-ui.js
> +%{_datadir}/kimchi/ui/js/jquery.min.js

Those 2 files were removed from kimchi code.

> +%{_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 1f193d0..8dd1db6 100644
> --- a/contrib/kimchi.spec.suse.in
> +++ b/contrib/kimchi.spec.suse.in
> @@ -96,6 +96,9 @@ rm -rf $RPM_BUILD_ROOT
>   %{_datadir}/kimchi/ui/images/theme-default/*.png
>   %{_datadir}/kimchi/ui/images/theme-default/*.gif
>   %{_datadir}/kimchi/ui/js/kimchi.min.js

> +%{_datadir}/kimchi/ui/js/jquery-ui.js
> +%{_datadir}/kimchi/ui/js/jquery.min.js

The same I commented above.


> +%{_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 a34da3a..5f6de5b 100644
> --- a/po/en_US.po
> +++ b/po/en_US.po
> @@ -1864,3 +1864,5 @@ msgstr "Clone"
>   
>   #~ msgid "Failed."
>   #~ msgstr "Failed."
> +msgid "Upload ISO Image"
> +msgstr "Upload ISO Image"

I think we can use "Upload file" as it can be used for any type of file

> diff --git a/po/pt_BR.po b/po/pt_BR.po
> index 452e778..a2b4aa7 100644
> --- a/po/pt_BR.po
> +++ b/po/pt_BR.po
> @@ -1928,3 +1928,6 @@ msgstr "Clonar"
>   
>   #~ msgid "Failed."
>   #~ msgstr "Falhou."
> +
> +msgid "Upload ISO Image"
> +msgstr "Carregar Imagem ISO"
> diff --git a/po/zh_CN.po b/po/zh_CN.po
> index 83c7018..6640032 100644
> --- a/po/zh_CN.po
> +++ b/po/zh_CN.po
> @@ -1794,3 +1794,6 @@ msgstr "制作副本"
>   
>   #~ msgid "Failed."
>   #~ msgstr "失败"
> +
> +msgid "Upload ISO Image"
> +msgstr "上传ISO镜像"
> diff --git a/src/kimchi/API.json b/src/kimchi/API.json
> index c3fc5e3..520b1d2 100644
> --- a/src/kimchi/API.json
> +++ b/src/kimchi/API.json
> @@ -162,7 +162,6 @@
>                       "description": "The name of the Storage Volume",
>                       "type": "string",
>                       "minLength": 1,
> -                    "required": true,
>                       "error": "KCHVOL0013E"
>                   },
>                   "allocation": {
> diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py
> index 460beb1..16abc85 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
> diff --git a/src/nginx.conf.in b/src/nginx.conf.in
> index 1d1a398..cd54ddc 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;
>   
>       # Timeout set to 10 minutes to avoid the 504 Gateway Timeout
>       # when Kimchi is processing a request.
> 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

I suggest to create a specific dir to include it.

ui/css/theme-default is used for Kimchi file as this one is imported it 
would be good to get it in a specific location.

I also would suggest to have a specific commit for imported file that 
way we can easily review the real code.

> @@ -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 @@

Same I commented above.

> +/*
> +* 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 0306571..e10b357 100644
> --- a/ui/js/src/kimchi.template_add_main.js
> +++ b/ui/js/src/kimchi.template_add_main.js
> @@ -390,6 +390,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');
> +    });
> +

We have agreed to add the upload/download function to the Storage tab 
(in the actions menu)
So this code can be removed.

> +    var r = new Resumable({
> +        target:'storagepools/ISO/storagevolumes'
> +    });
> +
> +    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 7bdf441..4fc10e2 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 418c5e8..796beb0 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>




More information about the Kimchi-devel mailing list