[RFC] ticket of VM
by Sheldon
Now I have send a patch V1, no more comments.
These days, I talk with ZhengSheng about the ticket of VM.
Now we are change our design as follow for we should care the VMs
created by other tools.
1. make the ticket as the sub-resource of a VM.
support GET(lookup) and PUT(update) method.
2. we will not set expire for ticket.
3. kimchi will set a initial random password for VM when create it.
4. PUT(update) method can set a password for a VM created by other tool.
but if expire is set for this VM, kimchi will not change the password.
or kimchi can change the password but not change the expire.
5. when GET method to retrieve the password, if the VM is create by
other-tools.
And expire is set, kimchi raise http 400 error when timeout.
6. pass the ticket to vnc/spice websocket in cookie, not in URL.
vnc/spice login page get the ticket from cookie.
--
Thanks and best regards!
Sheldon Feng(冯少合)<shaohef(a)linux.vnet.ibm.com>
IBM Linux Technology Center
10 years, 5 months
[RFC] discover Kimchi peers
by Sheldon
I'd like to talk about how to discover Kimchi peers.
Now I just talk about discover a peer in a same network here.
I will use a local multicast subnetwork address
<http://en.wikipedia.org/wiki/Multicast_address#Local_subnetwork> to
find peers in one network.
Choose 224.0.0.132 as the kimchi multicast address, and 8000 as the port.
For cross network that need the router support multicast, so I give up
discover a peer cross network.
I will let user add the remote peers manully.
In the whole system all peers are equal. There will not be a center
discover service.
We will define two kinds of multicast message Notify and Search.
the format as follow.
# ____________________________________________
# | head | message body | tail |
# |___________________|_________________|______|
# |sizeof(messageBody)| json message | EOL |
# |___________________|_________________|______|
the flow of discover Kimchi peers:
1. one host multicast a "search" message. in this message it tell
himself information, and ask others tell their information.
Like this:
{"search": {"domain": "kimchi-host1", "IP": "192.168.0.3", "httpport":
"8000", "httpsport": "8001"}}
this means "hello, I'm a kimchi host, this is my information, can
you tell me who you are?"
2. others received a search will response an "notify" message.
Like this:
{"notify": {"domain": "kimchi-host1", "IP": "192.168.0.3",
"httpport": "8000", "httpsport": "8001"}}
this tells others that "hi, I'm a kimchi host, I'm alive and this
is my information"
o (user) 1 is "search"
/\ --------> 2 is "notify"
| 1 _____ ________________
kimchi-host1 ------2-<---| DB |<----2-----|multicast listen |<--2--
| |____| |_______________|
|
----------------------1-------------------------------> |
|
_____ |_____
| |
| switch |
| |
|___________|
o (user) |
/\ --------- -----------------------------2----------------------- --->|
| | _____ ________________ |
kimchi-host3 ------1<---| DB |<---1------|multicast listen |<---1--|
|____| |_______________|
we need to discuss:
1. should we support beat heart?
every kimchi host will send periodic notify to tell others himself
information?
2. should we support a "quit" message?
"quit" message tell other peers, that this kimchi quit normally. others
can remove this from peers list.
But the kimchi can not send "quit" when aborting abnormally.
3. Do we store the local peers information in DB?
we can collection the local peers and send them immediately when user
need to discover the peers.
We will extend to support remote peers, these information need to be
stored in DB.
--
Thanks and best regards!
Sheldon Feng(???)<shaohef(a)linux.vnet.ibm.com>
IBM Linux Technology Center
10 years, 5 months
[PATCH v2 0/2] Keep UI Consistent in Guest Edit Window
by Hongliang Wang
Make guest cdrom edit UI consistent with guest interface edit UI. The key point
is to edit cdrom properties in place. Because there is only one property can be
updated for a cdrom, it works fine this way. Though another inconsistence comes
up that in the same storage tab, there is also lines for disks, which have more
editable properties and it's not that easy to make all of these properties stay
in one line. So for disks, possibly we still need provide another window to let
users update properties.
So here are 2 choices:
C1) Apply this PATCH v2 to keep cdrom consistent with interface
C2) Apply PATCH v1 to keep cdrom consistent with disk
Both are OK for me.
v1 -> v2:
2a) Made updating cdrom properties in place
(Aline's comment)
Hongliang Wang (2):
Adjust Guest Edit Storage Tab Styles
Remove Unused Files
ui/css/theme-default/guest-cdrom-edit.css | 57 ------------
ui/css/theme-default/guest-edit.css | 99 +++++++-------------
ui/images/theme-default/guest-icon-sprite.png | Bin 6748 -> 0 bytes
ui/js/src/kimchi.guest_cdrom_edit_main.js | 85 ------------------
ui/js/src/kimchi.guest_edit_main.js | 125 +++++++++++++++++++++-----
ui/pages/guest-cdrom-edit.html.tmpl | 70 ---------------
ui/pages/guest-edit.html.tmpl | 57 +++++++-----
7 files changed, 166 insertions(+), 327 deletions(-)
delete mode 100644 ui/css/theme-default/guest-cdrom-edit.css
delete mode 100644 ui/images/theme-default/guest-icon-sprite.png
delete mode 100644 ui/js/src/kimchi.guest_cdrom_edit_main.js
delete mode 100644 ui/pages/guest-cdrom-edit.html.tmpl
--
1.8.1.4
10 years, 5 months
[RFC] Improve task management for kimchi
by Wen Wang
Dear all,
*
**Problems:*
Now our strategy for long time operation is using task which the
browser needs to check up-to-date task status time by time until the
task ends. It's time consuming and less efficient. Also there exists
several problems when locating each task when doing debug generating and
storage pool as well as some new features that might use task strategy
in the future.
*Solution*:
As talked with Sheldon and Zhengsheng, we came up with a solution that
avoid browser checking status every 200ms. Also, we might need some more
labels in each task to provide more information when getting the task
like we might need to indicate which operation triggered certain task.
What's in our mind is to use the strategy that allow the server inform
browser about the task information. Our proposal is designed as follows.
1) Browser needs to register to the back end to indicate which part the
result needs to reply to when the task finished.
2) The back end use broker to manage message distribution: when a task
is finished or experiencing an error, back end inform the browser
certain part of work is finished or error.
3) Using websocket of cherrypy to accomplish the message transfer.
Best Regards
Wang Wen
10 years, 5 months
RFC: smt support
by Christy Perez
Quick poll for adding support for
<cpu>
<topology sockets='1' cores='2' threads='1'/>
</cpu>
I know it would be nice to let users add it on a per-vm basis, but maybe
it might be something that we'd rather just leave in the template only.
Having it in the template only seems simpler when thinking about UI
design, imo, but I'd love to hear what everyone else thinks.
Thanks,
- Christy
10 years, 5 months
[PATCH 1/2] add SysV init scripts in centos6
by ssdxiao
now in centos6 has'nt the SysV init scripts, so add it to kimchi
Signed-off-by: Ding Xiao <ssdxiao(a)163.com>
---
contrib/kimchid.sysvinit.centos6 | 112 ++++++++++++++++++++++++++++++++++++++
1 files changed, 112 insertions(+), 0 deletions(-)
create mode 100644 contrib/kimchid.sysvinit.centos6
diff --git a/contrib/kimchid.sysvinit.centos6 b/contrib/kimchid.sysvinit.centos6
new file mode 100644
index 0000000..ba4adff
--- /dev/null
+++ b/contrib/kimchid.sysvinit.centos6
@@ -0,0 +1,112 @@
+#! /bin/sh
+#
+#kimchid Kimchi Web Server
+#
+#
+# Author: Ding Xiao <ssdxiao(a)163.com>
+#
+# 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
+#
+### BEGIN INIT INFO
+# Provides: kimchid
+# Required-Start: libvirtd
+# Required-Stop:
+# Default-Start: 3 5
+# Default-Stop: 0 1 2 6
+# Description: Start the kimchid daemon
+### END INIT INFO
+
+. /etc/rc.d/init.d/functions
+
+prog="kimchid"
+exec="/usr/bin/kimchid"
+pidfile="/var/run/kimchi.pid"
+
+
+lockfile=/var/lock/subsys/$prog
+
+start() {
+ [ -x $exec ] || exit 5
+ echo -n $"Starting $prog: "
+ daemon --user root --pidfile $pidfile "$exec &>/dev/null & echo \$! > $pidfile"
+ retval=$?
+ echo
+ [ $retval -eq 0 ] && touch $lockfile
+ return $retval
+}
+
+stop() {
+ echo -n $"Stopping $prog: "
+ killproc -p $pidfile $prog
+ retval=$?
+ echo
+ [ $retval -eq 0 ] && rm -f $lockfile
+ return $retval
+}
+
+restart() {
+ stop
+ start
+}
+
+reload() {
+ restart
+}
+
+force_reload() {
+ restart
+}
+
+rh_status() {
+ status -p $pidfile $prog
+}
+
+rh_status_q() {
+ rh_status >/dev/null 2>&1
+}
+
+
+case "$1" in
+ start)
+ rh_status_q && exit 0
+ $1
+ ;;
+ stop)
+ rh_status_q || exit 0
+ $1
+ ;;
+ restart)
+ $1
+ ;;
+ reload)
+ rh_status_q || exit 7
+ $1
+ ;;
+ force-reload)
+ force_reload
+ ;;
+ status)
+ rh_status
+ ;;
+ condrestart|try-restart)
+ rh_status_q || exit 0
+ restart
+ ;;
+ *)
+ echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}"
+ exit 2
+esac
+exit $?
+
--
1.7.1
10 years, 5 months
[PATCH] Add SUSE's products
by Dinar valeev
From: Dinar Valeev <dvaleev(a)suse.com>
Add SLES 12 information and set openSUSE's version to 13.1
Signed-off-by: Dinar Valeev <dvaleev(a)suse.com>
---
src/kimchi/isoinfo.py | 1 +
src/kimchi/osinfo.py | 9 ++++++---
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/src/kimchi/isoinfo.py b/src/kimchi/isoinfo.py
index b7315e0..c394a32 100644
--- a/src/kimchi/isoinfo.py
+++ b/src/kimchi/isoinfo.py
@@ -95,6 +95,7 @@ iso_dir = [
'|HRM_CENA_X64CHKV|HRM_CPRA_X64FREV|HRM_CPRNA_X64FREV')),
('sles', '10', 'SLES10|SUSE-Linux-Enterprise-Server.001'),
('sles', '11', 'SUSE_SLES-11-0-0'),
+ ('sles', '12', 'SLE-12'),
('sles', lambda m: "11sp%s" % m.group(1), 'SLES-11-SP(\d+)'),
('opensuse', lambda m: m.group(1), 'openSUSE[ -](\d+\.\d+)'),
('opensuse', '11.1', 'SU1110.001'),
diff --git a/src/kimchi/osinfo.py b/src/kimchi/osinfo.py
index 093feca..9e8b62e 100644
--- a/src/kimchi/osinfo.py
+++ b/src/kimchi/osinfo.py
@@ -57,10 +57,13 @@ template_specs = {'x86': {'old': dict(common_spec, disk_bus='ide',
modern_version_bases = {'x86': {'debian': '6.0', 'ubuntu': '7.10',
- 'opensuse': '10.3', 'centos': '5.3',
- 'rhel': '6.0', 'fedora': '16', 'gentoo': '0'},
+ 'opensuse': '13.1', 'centos': '5.3',
+ 'rhel': '6.0', 'fedora': '16', 'gentoo': '0',
+ 'sles': '12'},
'power': {'rhel': '7.0', 'fedora': '19',
- 'ubuntu': '14.04'}}
+ 'ubuntu': '14.04',
+ 'opensuse': '13.1',
+ 'sles': '12'}}
icon_available_distros = [icon[5:-4] for icon in glob.glob1('%s/images/'
% paths.ui_dir, 'icon-*.png')]
--
1.8.4.5
10 years, 5 months
RFC: Security Model & UI Design
by Yu Xin Huo
*Security Strategy:*
1. Only handle existing linux users and groups, kimchi is positioned to
be a virtualization console, will not handle user management which is
host level admin.
2. Two levels of privileges
root users: console settings and virtualization resources
management
full access to 'Host', 'Guests', 'Templates',
'Storage', 'Network'
all root users can see all the guests, templates,
storage pools and volumes, networks no matter who created it
for created VMs, assign to non-root users with
either an admin or user role
non-root users: manage or use VMs assigned to them
admin role: edit & delete their VMs
user role: start, stop, vnc their VMs
they only have access to 'Guests' tab
In 'Guests' tab, only list VMs that they have an
admin or user role
*UI Design:*
root users:
all current UI will be available.
for create a VM, add a section to add users with admin or user role
for edit a VM, also has a section for add/remove/change users'
access
non-root users:
As only one 'Guest' tab, remove tabs bar and the '+' bar
Only list VMs that they have a role on
If the user have 'admin' role, then all current actions available
if the user have 'user' role, then only actions 'start',
'stop', 'vnc' available
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
[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