[RFC] Upload and download image for OOTB storage pool
by Royce Lv
Now OOTB ISO pool is available, but user has to copy ISO manually to the
ISO pool, which is not convenient.
So here we propose two ways to transfer ISOs.
1. User ---upload --> Kimchi Server
REST API will be:
POST /storagepools/isos/upload
{'file': file_local_abs_path}
Ding Xiao had already posted a piece of code in his patch:
using cherrypy file upload interface. His code can support following
requirements:
(1) upload task can be cancelled.
(2) non-blocking, other request can be handled.
(3) progress can be monitored with a progress bar.
(4) multi-file selected support.
Only nits are UI part needs to be refactored a little.
Also for multiple file upload, number of upload session needs to be
controlled through config file.
2. Http server -- download -->Kimchi server
REST API will be:
POST /storagepools/isos/download
{'file': file_remote_path}
RETURN:
{'task_id': id_of_download_task}
This will start a backend task for download. The UI will query the task
for progress.
For implementation, we will use python libcurl to download image/ISO.
REF:
http://curl.haxx.se/libcurl/python/
10 years, 5 months
[PATCH] Add image probe function
by lvroyce@linux.vnet.ibm.com
From: Royce Lv <lvroyce(a)linux.vnet.ibm.com>
Image file probe will be used in identify image file os info and
generate reasonable configuration for it.
This will be useful when import image and create a vm from it.
Signed-off-by: Royce Lv <lvroyce(a)linux.vnet.ibm.com>
---
docs/README.md | 9 ++++++---
src/kimchi/exception.py | 2 ++
src/kimchi/imageinfo.py | 42 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 50 insertions(+), 3 deletions(-)
create mode 100644 src/kimchi/imageinfo.py
diff --git a/docs/README.md b/docs/README.md
index c658637..c341a5d 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -53,7 +53,8 @@ Install Dependencies
PyPAM m2crypto python-jsonschema rpm-build \
qemu-kvm python-psutil python-ethtool sos \
python-ipaddr python-lxml nfs-utils \
- iscsi-initiator-utils libxslt pyparted nginx
+ iscsi-initiator-utils libxslt pyparted nginx \
+ python-libguestfs libguestfs-tools
# If using RHEL6, install the following additional packages:
$ sudo yum install python-unittest2 python-ordereddict
# Restart libvirt to allow configuration changes to take effect
@@ -75,7 +76,8 @@ for more information on how to configure your system to access this repository.
python-pam python-m2crypto python-jsonschema \
qemu-kvm libtool python-psutil python-ethtool \
sosreport python-ipaddr python-lxml nfs-common \
- open-iscsi lvm2 xsltproc python-parted nginx
+ open-iscsi lvm2 xsltproc python-parted nginx \
+ python-guestfs libguestfs-tools
Packages version requirement:
python-jsonschema >= 1.3.0
@@ -89,7 +91,8 @@ for more information on how to configure your system to access this repository.
python-pam python-M2Crypto python-jsonschema \
rpm-build kvm python-psutil python-ethtool \
python-ipaddr python-lxml nfs-client open-iscsi \
- libxslt-tools python-xml python-parted
+ libxslt-tools python-xml python-parted \
+ python-libguestfs guestfs-tools
Packages version requirement:
python-psutil >= 0.6.0
diff --git a/src/kimchi/exception.py b/src/kimchi/exception.py
index fcf60cc..a983d46 100644
--- a/src/kimchi/exception.py
+++ b/src/kimchi/exception.py
@@ -88,6 +88,8 @@ class InvalidOperation(KimchiException):
class IsoFormatError(KimchiException):
pass
+class ImageFormatError(KimchiException):
+ pass
class TimeoutExpired(KimchiException):
pass
diff --git a/src/kimchi/imageinfo.py b/src/kimchi/imageinfo.py
new file mode 100644
index 0000000..d57ecac
--- /dev/null
+++ b/src/kimchi/imageinfo.py
@@ -0,0 +1,42 @@
+#
+# Kimchi
+#
+# Copyright IBM Corp, 2013
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+import sys
+import guestfs
+
+
+def probe_image(image_path):
+ g = guestfs.GuestFS(python_return_dict=True)
+ g.add_drive_opts(image_path, readonly=1)
+ g.launch()
+
+ roots = g.inspect_os()
+ if len(roots) == 0:
+ raise ImageFormatError("No os found in given image.")
+
+ for root in roots:
+ version = "%d.%d" % (g.inspect_get_major_version(root),
+ g.inspect_get_minor_version(root))
+ distro = "%s" % (g.inspect_get_distro(root))
+
+ return (distro, version)
+
+
+if __name__ == '__main__':
+ print probe_image(sys.argv[1])
--
1.8.3.2
10 years, 5 months
RFC: Suggestion of changing the button "Clone" to "Replicate"
by Wen Wang
Dear all,
I did some translation in last release and found out it is a little bit
odd to see the word "clone" under "Action" in Templates tab:
In my point of view, "clone" is used in medical area in most cases and I
recommend to have it changed to "Replicate" which have the same meaning
that indicates to copy and paste the same template configuration.
Best Regards
Wang Wen
10 years, 5 months
[PATCH] Support to upload ISO
by ssdxiao
Upload ISO to the path /var/lib/kimchi/iso of the local disk
Signed-off-by: ssdxiao <ssdxiao(a)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(a)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
10 years, 5 months
UI developmet help to fix bug
by Paulo Ricardo Paz Vital
Hello UI developers.
I'd like to ask for one volunteer to help me on coding the UI part for
the solution of bugzilla 110428 [1]. This solution will be submitted to
Kimchi project.
I already have the backend done and tried to do something nice and
beautiful, but I'm taking too much time in this part and want some help
from you guys.
[1] https://bugzilla.linux.ibm.com/show_bug.cgi?id=110428
Thanks and best regards,
--
Paulo Ricardo Paz Vital <pvital(a)linux.vnet.ibm.com>
IBM Linux Technology Center
10 years, 5 months