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

Aline Manera alinefm at linux.vnet.ibm.com
Wed Jun 11 17:20:37 UTC 2014


Welcome to Kimchi, ssdxiao!
It is always a pleasure to see new faces around here.

I've just applied your patches and made some tests.
I noticed you are not uploading new files but instead of that, you copy 
local ISOs files to a new pool.

As Royce mentioned, she is creating a ootb pool to guide users to save 
their ISOs there.
So those ISOs can easily be found by shallow scan.

By uploading, I'd except to user provides a remote URL to Kimchi 
downloads the ISO and upload it to that pool.

So we have 2 scenarios:
1) local -> local
    I am sure it is useful to user when he has the ootb pool.
    And it also duplicates the ISOs in different places in the host system
    In my mind, it would be better if the sysadmin moves the ISOs he 
wants to use in Kimchi to the ootb pool dir

2) remote -> local
    It means download an ISO from a remote URL and upload it to the ootb 
pool
    That way user doesn't need to do it manually if he doesn't want to 
use ISO streaming.


On 06/09/2014 11:56 PM, ssdxiao wrote:
> 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>




More information about the Kimchi-devel mailing list