[WIP][PATCH 0/4] Support upload volume to storagepool

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Note: The UI patch is just used for testing. Problem remains: 1. POST return 404 because the UI patch does not include accept json 2. task id does not returned in vol data and progress has not been added 3. Haven't tested large file, so not sure whether we need to split large file into peices and call POST multi-times. Royce Lv (3): Storage volume upload: Dispatch volume create to right handler Storage volume upload: Parse params for upload formdata Storage volume upload: add model function of upload ssdxiao (1): Support to upload ISO contrib/kimchi.spec.fedora.in | 3 + contrib/kimchi.spec.suse.in | 3 + po/en_US.po | 2 + po/pt_BR.po | 3 + po/zh_CN.po | 3 + src/kimchi/API.json | 1 - src/kimchi/control/storagepools.py | 4 +- src/kimchi/control/utils.py | 2 + src/kimchi/i18n.py | 1 + src/kimchi/model/storagevolumes.py | 34 ++ 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 + 16 files changed, 954 insertions(+), 3 deletions(-) create mode 100644 ui/css/theme-default/upload.css create mode 100644 ui/js/resumable.js -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> As we are starting to support upload and download to create volume, they need to be distinguished from previous creating through libvirt api. Adding a dispatcher to support this. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/i18n.py | 1 + src/kimchi/model/storagevolumes.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 2eae7e8..26b916b 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -185,6 +185,7 @@ messages = { "KCHVOL0015E": _("Storage volume format not supported"), "KCHVOL0016E": _("Storage volume requires a volume name"), "KCHVOL0017E": _("Unable to update database with storage volume information due error: %(err)s"), + "KCHVOL0018E": _("Only one of %(param)s can be specified"), "KCHIFACE0001E": _("Interface %(name)s does not exist"), diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index b60884c..a7823c2 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -44,6 +44,17 @@ class StorageVolumesModel(object): self.objstore = kargs['objstore'] def create(self, pool_name, params): + vol_source = ['file', 'url', 'capacity'] + + if sum(1 for p in vol_source if p in params) != 1: + raise InvalidParameter("KCHVOL0018E", {'param': str(vol_source)}) + + for p in vol_source: + create_func = getattr(self, "_create_volume_with_" + p, None) + if create_func and p in params: + return create_func(pool_name, params) + + def _create_volume_with_capacity(self, pool_name, params): vol_xml = """ <volume> <name>%(name)s</name> -- 1.8.3.2

On 08/21/2014 11:42 AM, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
As we are starting to support upload and download to create volume, they need to be distinguished from previous creating through libvirt api. Adding a dispatcher to support this.
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/i18n.py | 1 + src/kimchi/model/storagevolumes.py | 11 +++++++++++ 2 files changed, 12 insertions(+)
diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 2eae7e8..26b916b 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -185,6 +185,7 @@ messages = { "KCHVOL0015E": _("Storage volume format not supported"), "KCHVOL0016E": _("Storage volume requires a volume name"), "KCHVOL0017E": _("Unable to update database with storage volume information due error: %(err)s"), + "KCHVOL0018E": _("Only one of %(param)s can be specified"),
From the code above, %(param)s will be always "file, url, capacity" so we can include them to the string and avoid the placeholder
"KCHIFACE0001E": _("Interface %(name)s does not exist"),
diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index b60884c..a7823c2 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -44,6 +44,17 @@ class StorageVolumesModel(object): self.objstore = kargs['objstore']
def create(self, pool_name, params): + vol_source = ['file', 'url', 'capacity'] + + if sum(1 for p in vol_source if p in params) != 1: + raise InvalidParameter("KCHVOL0018E", {'param': str(vol_source)}) + + for p in vol_source: + create_func = getattr(self, "_create_volume_with_" + p, None) + if create_func and p in params: + return create_func(pool_name, params) + + def _create_volume_with_capacity(self, pool_name, params): vol_xml = """ <volume> <name>%(name)s</name>

On 21-08-2014 15:55, Aline Manera wrote:
diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 2eae7e8..26b916b 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -185,6 +185,7 @@ messages = { "KCHVOL0015E": _("Storage volume format not supported"), "KCHVOL0016E": _("Storage volume requires a volume name"), "KCHVOL0017E": _("Unable to update database with storage volume information due error: %(err)s"), + "KCHVOL0018E": _("Only one of %(param)s can be specified"),
From the code above, %(param)s will be always "file, url, capacity" so we can include them to the string and avoid the placeholder
The dict "vol_source" is also used somewhere else in the code other than filling this placeholder (i.e. to validate the parameters) so I think it's worth to have its values outside the error string.

On 2014年08月27日 01:52, Crístian Viana wrote:
On 21-08-2014 15:55, Aline Manera wrote:
diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 2eae7e8..26b916b 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -185,6 +185,7 @@ messages = { "KCHVOL0015E": _("Storage volume format not supported"), "KCHVOL0016E": _("Storage volume requires a volume name"), "KCHVOL0017E": _("Unable to update database with storage volume information due error: %(err)s"), + "KCHVOL0018E": _("Only one of %(param)s can be specified"),
From the code above, %(param)s will be always "file, url, capacity" so we can include them to the string and avoid the placeholder
The dict "vol_source" is also used somewhere else in the code other than filling this placeholder (i.e. to validate the parameters) so I think it's worth to have its values outside the error string. Yeah, aggregating them in "vol_source" is for future extension. If we support another type of volume creation method (let's say creating a volume base on another volume), we don't need to change this error message and elsewhere using it, we just add another value in "vol_source" list and everything can work as what it is now.
Does that make sense?
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

On 21-08-2014 11:42, lvroyce0210@gmail.com wrote:
+ for p in vol_source: + create_func = getattr(self, "_create_volume_with_" + p, None) + if create_func and p in params: + return create_func(pool_name, params) + There needs to be a better error handling here. What if there's no appropriate function (create_volume_with_XXX)? The function create won't return anything and nothing will be reported. Actually, I guess an error will be raised later because whoever calls the function "create" expects a string in return.

On 2014年08月22日 05:02, Crístian Viana wrote:
On 21-08-2014 11:42, lvroyce0210@gmail.com wrote:
+ for p in vol_source: + create_func = getattr(self, "_create_volume_with_" + p, None) + if create_func and p in params: + return create_func(pool_name, params) + There needs to be a better error handling here. What if there's no appropriate function (create_volume_with_XXX)? The function create won't return anything and nothing will be reported. Actually, I guess an error will be raised later because whoever calls the function "create" expects a string in return. Cristian,
Thanks for your comments! This part of error handling is here: + vol_source = ['file', 'url', 'capacity'] + + if sum(1 for p in vol_source if p in params) != 1: + raise InvalidParameter("KCHVOL0018E", {'param': str(vol_source)}) These lines guarantee one and only one of these params are specified. Still I need to add a line of "def create_volume_with_url(self): pass" to make sure the problem you mentioned will be avoided.
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

On 25-08-2014 04:29, Royce Lv wrote:
Cristian,
Thanks for your comments!
This part of error handling is here: + vol_source = ['file', 'url', 'capacity'] + + if sum(1 for p in vol_source if p in params) != 1: + raise InvalidParameter("KCHVOL0018E", {'param': str(vol_source)})
These lines guarantee one and only one of these params are specified.
Exactly. But they don't guarantee that the corresponding function (let's say, _create_volume_with_url) exists.
Still I need to add a line of "def create_volume_with_url(self): pass" to make sure the problem you mentioned will be avoided.
Sure, that will avoid the problem for now, but the function "create" still isn't sure whether the other functions exist. It just expects so. It's better to raise an error if that function can't find the other functions rather than fail silently (by not running any function).

From: Royce Lv <lvroyce@linux.vnet.ibm.com> When pass form to cherrypy, it is stored in cherrypy.request.params, so adjust parse_request for this change. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/control/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/kimchi/control/utils.py b/src/kimchi/control/utils.py index fbd5177..8620c1d 100644 --- a/src/kimchi/control/utils.py +++ b/src/kimchi/control/utils.py @@ -80,6 +80,8 @@ def parse_request(): except ValueError: e = OperationFailed('KCHAPI0006E') raise cherrypy.HTTPError(400, e.message) + elif mime_in_header('Content-Type', 'multipart/form-data'): + return cherrypy.request.params else: e = OperationFailed('KCHAPI0007E') raise cherrypy.HTTPError(415, e.message) -- 1.8.3.2

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 08/21/2014 11:42 AM, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
When pass form to cherrypy, it is stored in cherrypy.request.params, so adjust parse_request for this change.
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/control/utils.py | 2 ++ 1 file changed, 2 insertions(+)
diff --git a/src/kimchi/control/utils.py b/src/kimchi/control/utils.py index fbd5177..8620c1d 100644 --- a/src/kimchi/control/utils.py +++ b/src/kimchi/control/utils.py @@ -80,6 +80,8 @@ def parse_request(): except ValueError: e = OperationFailed('KCHAPI0006E') raise cherrypy.HTTPError(400, e.message) + elif mime_in_header('Content-Type', 'multipart/form-data'): + return cherrypy.request.params else: e = OperationFailed('KCHAPI0007E') raise cherrypy.HTTPError(415, e.message)

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Add model function to upload file Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/model/storagevolumes.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index a7823c2..0b9d96a 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -54,6 +54,29 @@ class StorageVolumesModel(object): if create_func and p in params: return create_func(pool_name, params) + def _create_volume_with_file(self, pool_name, params): + pool = StoragePoolModel.get_storagepool(pool_name, self.conn) + dir_path = StoragePoolModel(conn=self.conn, + objstore=self.objstore).lookup(pool_name)['path'] + upload_file = params['file'] + file_name = params['resumableFilename'] + size = 0 + allData='' + while True: + data = upload_file.file.read(8192) + allData += data + if not data: + break + size += len(data) + + file_path = os.path.join(dir_path, file_name) + f = open(file_path, 'wb') + f.write(allData) + f.close() + # Refresh to make sure volume can be found in following lookup + pool.refresh() + return file_name + def _create_volume_with_capacity(self, pool_name, params): vol_xml = """ <volume> -- 1.8.3.2

On 08/21/2014 11:43 AM, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Add model function to upload file
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/model/storagevolumes.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+)
diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index a7823c2..0b9d96a 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -54,6 +54,29 @@ class StorageVolumesModel(object): if create_func and p in params: return create_func(pool_name, params)
+ def _create_volume_with_file(self, pool_name, params):
+ pool = StoragePoolModel.get_storagepool(pool_name, self.conn)
I'd suggest to get the pool info right before using it.
+ dir_path = StoragePoolModel(conn=self.conn, + objstore=self.objstore).lookup(pool_name)['path'] + upload_file = params['file'] + file_name = params['resumableFilename'] + size = 0 + allData=''
+ while True: + data = upload_file.file.read(8192) + allData += data + if not data: + break + size += len(data) + + file_path = os.path.join(dir_path, file_name) + f = open(file_path, 'wb') + f.write(allData) + f.close()
This file write code can raise some exceptions, like if the disk is full or something like that It would be good to use try/except to handle those possible problems.
+ # Refresh to make sure volume can be found in following lookup + pool.refresh() + return file_name + def _create_volume_with_capacity(self, pool_name, params): vol_xml = """ <volume>

On 2014年08月22日 03:00, Aline Manera wrote:
On 08/21/2014 11:43 AM, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Add model function to upload file
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/model/storagevolumes.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+)
diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index a7823c2..0b9d96a 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -54,6 +54,29 @@ class StorageVolumesModel(object): if create_func and p in params: return create_func(pool_name, params)
+ def _create_volume_with_file(self, pool_name, params):
+ pool = StoragePoolModel.get_storagepool(pool_name, self.conn)
I'd suggest to get the pool info right before using it.
+ dir_path = StoragePoolModel(conn=self.conn, + objstore=self.objstore).lookup(pool_name)['path'] + upload_file = params['file'] + file_name = params['resumableFilename'] + size = 0 + allData=''
+ while True: + data = upload_file.file.read(8192) + allData += data + if not data: + break + size += len(data) + + file_path = os.path.join(dir_path, file_name) + f = open(file_path, 'wb') + f.write(allData) + f.close()
This file write code can raise some exceptions, like if the disk is full or something like that It would be good to use try/except to handle those possible problems.
Of course, this is kind of demo here to make sure we like the api definition;), I would like to add big file support and catch the exceptions.
+ # Refresh to make sure volume can be found in following lookup + pool.refresh() + return file_name + def _create_volume_with_capacity(self, pool_name, params): vol_xml = """ <volume>
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

On 21-08-2014 11:43, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Add model function to upload file
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/model/storagevolumes.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+)
diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index a7823c2..0b9d96a 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -54,6 +54,29 @@ class StorageVolumesModel(object): if create_func and p in params: return create_func(pool_name, params)
+ def _create_volume_with_file(self, pool_name, params): + pool = StoragePoolModel.get_storagepool(pool_name, self.conn) + dir_path = StoragePoolModel(conn=self.conn, + objstore=self.objstore).lookup(pool_name)['path'] + upload_file = params['file'] + file_name = params['resumableFilename'] + size = 0 + allData='' + while True: + data = upload_file.file.read(8192) + allData += data + if not data: + break + size += len(data) + + file_path = os.path.join(dir_path, file_name) + f = open(file_path, 'wb') + f.write(allData) + f.close() + # Refresh to make sure volume can be found in following lookup + pool.refresh() + return file_name + def _create_volume_with_capacity(self, pool_name, params): vol_xml = """ <volume>
BTW, shouldn't this operation return a Task?

On 08/26/2014 10:42 AM, Crístian Viana wrote:
On 21-08-2014 11:43, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Add model function to upload file
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/model/storagevolumes.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+)
diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index a7823c2..0b9d96a 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -54,6 +54,29 @@ class StorageVolumesModel(object): if create_func and p in params: return create_func(pool_name, params)
+ def _create_volume_with_file(self, pool_name, params): + pool = StoragePoolModel.get_storagepool(pool_name, self.conn) + dir_path = StoragePoolModel(conn=self.conn, + objstore=self.objstore).lookup(pool_name)['path'] + upload_file = params['file'] + file_name = params['resumableFilename'] + size = 0 + allData='' + while True: + data = upload_file.file.read(8192) + allData += data + if not data: + break + size += len(data) + + file_path = os.path.join(dir_path, file_name) + f = open(file_path, 'wb') + f.write(allData) + f.close() + # Refresh to make sure volume can be found in following lookup + pool.refresh() + return file_name + def _create_volume_with_capacity(self, pool_name, params): vol_xml = """ <volume>
BTW, shouldn't this operation return a Task?
Yeap! upload and download are time consuming action and should return a Task
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

From: ssdxiao <ssdxiao@163.com> Upload ISO to the path /var/lib/kimchi/iso of the local disk Signed-off-by: ssdxiao <ssdxiao@163.com> Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- contrib/kimchi.spec.fedora.in | 3 + contrib/kimchi.spec.suse.in | 3 + po/en_US.po | 2 + po/pt_BR.po | 3 + po/zh_CN.po | 3 + src/kimchi/API.json | 1 - src/kimchi/control/storagepools.py | 4 +- src/nginx.conf.in | 1 + ui/css/theme-default/upload.css | 43 ++ ui/js/resumable.js | 816 ++++++++++++++++++++++++++++++++++ ui/js/src/kimchi.template_add_main.js | 27 ++ ui/pages/kimchi-ui.html.tmpl | 1 + ui/pages/template-add.html.tmpl | 13 + 13 files changed, 917 insertions(+), 3 deletions(-) create mode 100644 ui/css/theme-default/upload.css create mode 100644 ui/js/resumable.js diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 5766784..6be0a26 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -175,6 +175,9 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/kimchi/ui/images/theme-default/*.png %{_datadir}/kimchi/ui/images/theme-default/*.gif %{_datadir}/kimchi/ui/js/kimchi.min.js +%{_datadir}/kimchi/ui/js/jquery-ui.js +%{_datadir}/kimchi/ui/js/jquery.min.js +%{_datadir}/kimchi/ui/js/resumable.js %{_datadir}/kimchi/ui/js/modernizr.custom.2.6.2.min.js %{_datadir}/kimchi/ui/js/novnc/*.js %{_datadir}/kimchi/ui/js/spice/*.js diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 1f193d0..8dd1db6 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -96,6 +96,9 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/kimchi/ui/images/theme-default/*.png %{_datadir}/kimchi/ui/images/theme-default/*.gif %{_datadir}/kimchi/ui/js/kimchi.min.js +%{_datadir}/kimchi/ui/js/jquery-ui.js +%{_datadir}/kimchi/ui/js/jquery.min.js +%{_datadir}/kimchi/ui/js/resumable.js %{_datadir}/kimchi/ui/js/modernizr.custom.2.6.2.min.js %{_datadir}/kimchi/ui/js/novnc/*.js %{_datadir}/kimchi/ui/js/spice/*.js diff --git a/po/en_US.po b/po/en_US.po index a34da3a..5f6de5b 100644 --- a/po/en_US.po +++ b/po/en_US.po @@ -1864,3 +1864,5 @@ msgstr "Clone" #~ msgid "Failed." #~ msgstr "Failed." +msgid "Upload ISO Image" +msgstr "Upload ISO Image" diff --git a/po/pt_BR.po b/po/pt_BR.po index 452e778..a2b4aa7 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1928,3 +1928,6 @@ msgstr "Clonar" #~ msgid "Failed." #~ msgstr "Falhou." + +msgid "Upload ISO Image" +msgstr "Carregar Imagem ISO" diff --git a/po/zh_CN.po b/po/zh_CN.po index 83c7018..6640032 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -1794,3 +1794,6 @@ msgstr "制作副本" #~ msgid "Failed." #~ msgstr "失败" + +msgid "Upload ISO Image" +msgstr "上传ISO镜像" diff --git a/src/kimchi/API.json b/src/kimchi/API.json index c3fc5e3..520b1d2 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -162,7 +162,6 @@ "description": "The name of the Storage Volume", "type": "string", "minLength": 1, - "required": true, "error": "KCHVOL0013E" }, "allocation": { diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index 460beb1..16abc85 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -18,8 +18,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import cherrypy - - +import os +import errno from kimchi.control.base import Collection, Resource from kimchi.control.storagevolumes import IsoVolumes, StorageVolumes from kimchi.control.utils import get_class_name, model_fn diff --git a/src/nginx.conf.in b/src/nginx.conf.in index 1d1a398..cd54ddc 100644 --- a/src/nginx.conf.in +++ b/src/nginx.conf.in @@ -37,6 +37,7 @@ http { access_log /var/log/nginx/access.log main; sendfile on; + client_max_body_size 2m; # Timeout set to 10 minutes to avoid the 504 Gateway Timeout # when Kimchi is processing a request. diff --git a/ui/css/theme-default/upload.css b/ui/css/theme-default/upload.css new file mode 100644 index 0000000..9cdfe4f --- /dev/null +++ b/ui/css/theme-default/upload.css @@ -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@23company.com +*/ + +(function(){ +"use strict"; + + var Resumable = function(opts){ + if ( !(this instanceof Resumable) ) { + return new Resumable(opts); + } + this.version = 1.0; + // SUPPORTED BY BROWSER? + // Check if these features are support by the browser: + // - File object type + // - Blob object type + // - FileList object type + // - slicing files + this.support = ( + (typeof(File)!=='undefined') + && + (typeof(Blob)!=='undefined') + && + (typeof(FileList)!=='undefined') + && + (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false) + ); + if(!this.support) return(false); + + + // PROPERTIES + var $ = this; + $.files = []; + $.defaults = { + chunkSize:1*1024*1024, + forceChunkSize:false, + simultaneousUploads:3, + fileParameterName:'file', + throttleProgressCallbacks:0.5, + query:{}, + headers:{}, + preprocess:null, + method:'multipart', + prioritizeFirstAndLastChunk:false, + target:'/', + testChunks:true, + generateUniqueIdentifier:null, + maxChunkRetries:undefined, + chunkRetryInterval:undefined, + permanentErrors:[404, 415, 500, 501], + maxFiles:undefined, + withCredentials:false, + xhrTimeout:0, + maxFilesErrorCallback:function (files, errorCount) { + var maxFiles = $.getOpt('maxFiles'); + alert('Please upload ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.'); + }, + minFileSize:1, + minFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.'); + }, + maxFileSize:undefined, + maxFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.'); + }, + fileType: [], + fileTypeErrorCallback: function(file, errorCount) { + alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.'); + } + }; + $.opts = opts||{}; + $.getOpt = function(o) { + var $opt = this; + // Get multiple option if passed an array + if(o instanceof Array) { + var options = {}; + $h.each(o, function(option){ + options[option] = $opt.getOpt(option); + }); + return options; + } + // Otherwise, just return a simple option + if ($opt instanceof ResumableChunk) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.fileObj; } + } + if ($opt instanceof ResumableFile) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.resumableObj; } + } + if ($opt instanceof Resumable) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { return $opt.defaults[o]; } + } + }; + + // EVENTS + // catchAll(event, ...) + // fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message), + // complete(), progress(), error(message, file), pause() + $.events = []; + $.on = function(event,callback){ + $.events.push(event.toLowerCase(), callback); + }; + $.fire = function(){ + // `arguments` is an object, not array, in FF, so: + var args = []; + for (var i=0; i<arguments.length; i++) args.push(arguments[i]); + // Find event listeners, and support pseudo-event `catchAll` + var event = args[0].toLowerCase(); + for (var i=0; i<=$.events.length; i+=2) { + if($.events[i]==event) $.events[i+1].apply($,args.slice(1)); + if($.events[i]=='catchall') $.events[i+1].apply(null,args); + } + if(event=='fileerror') $.fire('error', args[2], args[1]); + if(event=='fileprogress') $.fire('progress'); + }; + + + // INTERNAL HELPER METHODS (handy, but ultimately not part of uploading) + var $h = { + stopEvent: function(e){ + e.stopPropagation(); + e.preventDefault(); + }, + each: function(o,callback){ + if(typeof(o.length)!=='undefined') { + for (var i=0; i<o.length; i++) { + // Array or FileList + if(callback(o[i])===false) return; + } + } else { + for (i in o) { + // Object + if(callback(i,o[i])===false) return; + } + } + }, + generateUniqueIdentifier:function(file){ + var custom = $.getOpt('generateUniqueIdentifier'); + if(typeof custom === 'function') { + return custom(file); + } + var relativePath = file.webkitRelativePath||file.fileName||file.name; // Some confusion in different versions of Firefox + var size = file.size; + return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')); + }, + contains:function(array,test) { + var result = false; + + $h.each(array, function(value) { + if (value == test) { + result = true; + return false; + } + return true; + }); + + return result; + }, + formatSize:function(size){ + if(size<1024) { + return size + ' bytes'; + } else if(size<1024*1024) { + return (size/1024.0).toFixed(0) + ' KB'; + } else if(size<1024*1024*1024) { + return (size/1024.0/1024.0).toFixed(1) + ' MB'; + } else { + return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB'; + } + }, + getTarget:function(params){ + var target = $.getOpt('target'); + if(target.indexOf('?') < 0) { + target += '?'; + } else { + target += '&'; + } + return target + params.join('&'); + } + }; + + var onDrop = function(event){ + $h.stopEvent(event); + appendFilesFromFileList(event.dataTransfer.files, event); + }; + var onDragOver = function(e) { + e.preventDefault(); + }; + + // INTERNAL METHODS (both handy and responsible for the heavy load) + var appendFilesFromFileList = function(fileList, event){ + // check for uploading too many files + var errorCount = 0; + var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']); + if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) { + // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file + if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) { + $.removeFile($.files[0]); + } else { + o.maxFilesErrorCallback(fileList, errorCount++); + return false; + } + } + var files = []; + $h.each(fileList, function(file){ + var fileName = file.name.split('.'); + var fileType = fileName[fileName.length-1].toLowerCase(); + + if (o.fileType.length > 0 && !$h.contains(o.fileType, fileType)) { + o.fileTypeErrorCallback(file, errorCount++); + return false; + } + + if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) { + o.minFileSizeErrorCallback(file, errorCount++); + return false; + } + if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) { + o.maxFileSizeErrorCallback(file, errorCount++); + return false; + } + + // directories have size == 0 + if (!$.getFromUniqueIdentifier($h.generateUniqueIdentifier(file))) {(function(){ + var f = new ResumableFile($, file); + window.setTimeout(function(){ + $.files.push(f); + files.push(f); + f.container = (typeof event != 'undefined' ? event.srcElement : null); + $.fire('fileAdded', f, event) + },0); + })()}; + }); + window.setTimeout(function(){ + $.fire('filesAdded', files) + },0); + }; + + // INTERNAL OBJECT TYPES + function ResumableFile(resumableObj, file){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $._prevProgress = 0; + $.resumableObj = resumableObj; + $.file = file; + $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox + $.size = file.size; + $.relativePath = file.webkitRelativePath || $.fileName; + $.uniqueIdentifier = $h.generateUniqueIdentifier(file); + $._pause = false; + $.container = ''; + var _error = false; + + // Callback when something happens within the chunk + var chunkEvent = function(event, message){ + // event can be 'progress', 'success', 'error' or 'retry' + switch(event){ + case 'progress': + $.resumableObj.fire('fileProgress', $); + break; + case 'error': + $.abort(); + _error = true; + $.chunks = []; + $.resumableObj.fire('fileError', $, message); + break; + case 'success': + if(_error) return; + $.resumableObj.fire('fileProgress', $); // it's at least progress + if($.isComplete()) { + $.resumableObj.fire('fileSuccess', $, message); + } + break; + case 'retry': + $.resumableObj.fire('fileRetry', $); + break; + } + }; + + // Main code to set up a file object with chunks, + // packaged to be able to handle retries if needed. + $.chunks = []; + $.abort = function(){ + // Stop current uploads + var abortCount = 0; + $h.each($.chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + abortCount++; + } + }); + if(abortCount>0) $.resumableObj.fire('fileProgress', $); + }; + $.cancel = function(){ + // Reset this file to be void + var _chunks = $.chunks; + $.chunks = []; + // Stop current uploads + $h.each(_chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + $.resumableObj.uploadNextChunk(); + } + }); + $.resumableObj.removeFile($); + $.resumableObj.fire('fileProgress', $); + }; + $.retry = function(){ + $.bootstrap(); + var firedRetry = false; + $.resumableObj.on('chunkingComplete', function(){ + if(!firedRetry) $.resumableObj.upload(); + firedRetry = true; + }); + }; + $.bootstrap = function(){ + $.abort(); + _error = false; + // Rebuild stack of chunks from file + $.chunks = []; + $._prevProgress = 0; + var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor; + var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1); + for (var offset=0; offset<maxOffset; offset++) {(function(offset){ + window.setTimeout(function(){ + $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent)); + $.resumableObj.fire('chunkingProgress',$,offset/maxOffset); + },0); + })(offset)} + window.setTimeout(function(){ + $.resumableObj.fire('chunkingComplete',$); + },0); + }; + $.progress = function(){ + if(_error) return(1); + // Sum up progress across everything + var ret = 0; + var error = false; + $h.each($.chunks, function(c){ + if(c.status()=='error') error = true; + ret += c.progress(true); // get chunk progress relative to entire file + }); + ret = (error ? 1 : (ret>0.999 ? 1 : ret)); + ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused + $._prevProgress = ret; + return(ret); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.chunks, function(chunk){ + if(chunk.status()=='uploading') { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.isComplete = function(){ + var outstanding = false; + $h.each($.chunks, function(chunk){ + var status = chunk.status(); + if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { + outstanding = true; + return(false); + } + }); + return(!outstanding); + }; + $.pause = function(pause){ + if(typeof(pause)==='undefined'){ + $._pause = ($._pause ? false : true); + }else{ + $._pause = pause; + } + }; + $.isPaused = function() { + return $._pause; + }; + + + // Bootstrap and return + $.resumableObj.fire('chunkingStart', $); + $.bootstrap(); + return(this); + } + + function ResumableChunk(resumableObj, fileObj, offset, callback){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $.resumableObj = resumableObj; + $.fileObj = fileObj; + $.fileObjSize = fileObj.size; + $.fileObjType = fileObj.file.type; + $.offset = offset; + $.callback = callback; + $.lastProgressCallback = (new Date); + $.tested = false; + $.retries = 0; + $.pendingRetry = false; + $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished + + // Computed properties + var chunkSize = $.getOpt('chunkSize'); + $.loaded = 0; + $.startByte = $.offset*chunkSize; + $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); + if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { + // The last chunk will be bigger than the chunk size, but less than 2*chunkSize + $.endByte = $.fileObjSize; + } + $.xhr = null; + + // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session + $.test = function(){ + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + var testHandler = function(e){ + $.tested = true; + var status = $.status(); + if(status=='success') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.send(); + } + }; + $.xhr.addEventListener('load', testHandler, false); + $.xhr.addEventListener('error', testHandler, false); + + // Add data from the query options + var params = []; + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); + }); + // Add extra data to identify chunk + params.push(['resumableChunkNumber', encodeURIComponent($.offset+1)].join('=')); + params.push(['resumableChunkSize', encodeURIComponent($.getOpt('chunkSize'))].join('=')); + params.push(['resumableCurrentChunkSize', encodeURIComponent($.endByte - $.startByte)].join('=')); + params.push(['resumableTotalSize', encodeURIComponent($.fileObjSize)].join('=')); + params.push(['resumableType', encodeURIComponent($.fileObjType)].join('=')); + params.push(['resumableIdentifier', encodeURIComponent($.fileObj.uniqueIdentifier)].join('=')); + params.push(['resumableFilename', encodeURIComponent($.fileObj.fileName)].join('=')); + params.push(['resumableRelativePath', encodeURIComponent($.fileObj.relativePath)].join('=')); + // Append the relevant chunk and send it + $.xhr.open('GET', $h.getTarget(params)); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + $h.each($.getOpt('headers'), function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + $.xhr.send(null); + }; + + $.preprocessFinished = function(){ + $.preprocessState = 2; + $.send(); + }; + + // send() uploads the actual data in a POST call + $.send = function(){ + var preprocess = $.getOpt('preprocess'); + if(typeof preprocess === 'function') { + switch($.preprocessState) { + case 0: preprocess($); $.preprocessState = 1; return; + case 1: return; + case 2: break; + } + } + if($.getOpt('testChunks') && !$.tested) { + $.test(); + return; + } + + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + // Progress + $.xhr.upload.addEventListener('progress', function(e){ + if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) { + $.callback('progress'); + $.lastProgressCallback = (new Date); + } + $.loaded=e.loaded||0; + }, false); + $.loaded = 0; + $.pendingRetry = false; + $.callback('progress'); + + // Done (either done, failed or retry) + var doneHandler = function(e){ + var status = $.status(); + if(status=='success'||status=='error') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.callback('retry', $.message()); + $.abort(); + $.retries++; + var retryInterval = $.getOpt('chunkRetryInterval'); + if(retryInterval !== undefined) { + $.pendingRetry = true; + setTimeout($.send, retryInterval); + } else { + $.send(); + } + } + }; + $.xhr.addEventListener('load', doneHandler, false); + $.xhr.addEventListener('error', doneHandler, false); + + // Set up the basic query data from Resumable + var query = { + resumableChunkNumber: $.offset+1, + resumableChunkSize: $.getOpt('chunkSize'), + resumableCurrentChunkSize: $.endByte - $.startByte, + resumableTotalSize: $.fileObjSize, + resumableType: $.fileObjType, + resumableIdentifier: $.fileObj.uniqueIdentifier, + resumableFilename: $.fileObj.fileName, + resumableRelativePath: $.fileObj.relativePath, + resumableTotalChunks: $.fileObj.chunks.length + }; + // Mix in custom data + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + query[k] = v; + }); + + var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))), + bytes = $.fileObj.file[func]($.startByte,$.endByte), + data = null, + target = $.getOpt('target'); + + if ($.getOpt('method') === 'octet') { + // Add data from the query options + data = bytes; + var params = []; + $h.each(query, function(k,v){ + params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); + }); + target = $h.getTarget(params); + } else { + // Add data from the query options + data = new FormData(); + $h.each(query, function(k,v){ + data.append(k,v); + }); + data.append($.getOpt('fileParameterName'), bytes); + } + + $.xhr.open('POST', target); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + $h.each($.getOpt('headers'), function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + $.xhr.send(data); + }; + $.abort = function(){ + // Abort and reset + if($.xhr) $.xhr.abort(); + $.xhr = null; + }; + $.status = function(){ + // Returns: 'pending', 'uploading', 'success', 'error' + if($.pendingRetry) { + // if pending retry then that's effectively the same as actively uploading, + // there might just be a slight delay before the retry starts + return('uploading'); + } else if(!$.xhr) { + return('pending'); + } else if($.xhr.readyState<4) { + // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening + return('uploading'); + } else { + if($.xhr.status==200) { + // HTTP 200, perfect + return('success'); + } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { + // HTTP 415/500/501, permanent error + return('error'); + } else { + // this should never happen, but we'll reset and queue a retry + // a likely case for this would be 503 service unavailable + $.abort(); + return('pending'); + } + } + }; + $.message = function(){ + return($.xhr ? $.xhr.responseText : ''); + }; + $.progress = function(relative){ + if(typeof(relative)==='undefined') relative = false; + var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); + if($.pendingRetry) return(0); + var s = $.status(); + switch(s){ + case 'success': + case 'error': + return(1*factor); + case 'pending': + return(0*factor); + default: + return($.loaded/($.endByte-$.startByte)*factor); + } + }; + return(this); + } + + // QUEUE + $.uploadNextChunk = function(){ + var found = false; + + // In some cases (such as videos) it's really handy to upload the first + // and last chunk of a file quickly; this let's the server check the file's + // metadata and determine if there's even a point in continuing. + if ($.getOpt('prioritizeFirstAndLastChunk')) { + $h.each($.files, function(file){ + if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) { + file.chunks[0].send(); + found = true; + return(false); + } + if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) { + file.chunks[file.chunks.length-1].send(); + found = true; + return(false); + } + }); + if(found) return(true); + } + + // Now, simply look for the next, best thing to upload + $h.each($.files, function(file){ + if(file.isPaused()===false){ + $h.each(file.chunks, function(chunk){ + if(chunk.status()=='pending' && chunk.preprocessState === 0) { + chunk.send(); + found = true; + return(false); + } + }); + } + if(found) return(false); + }); + if(found) return(true); + + // The are no more outstanding chunks to upload, check is everything is done + var outstanding = false; + $h.each($.files, function(file){ + if(!file.isComplete()) { + outstanding = true; + return(false); + } + }); + if(!outstanding) { + // All chunks have been uploaded, complete + $.fire('complete'); + } + return(false); + }; + + + // PUBLIC METHODS FOR RESUMABLE.JS + $.assignBrowse = function(domNodes, isDirectory){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + var input; + if(domNode.tagName==='INPUT' && domNode.type==='file'){ + input = domNode; + } else { + input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.style.display = 'none'; + domNode.addEventListener('click', function(){ + input.style.opacity = 0; + input.style.display='block'; + input.focus(); + input.click(); + input.style.display='none'; + }, false); + domNode.appendChild(input); + } + var maxFiles = $.getOpt('maxFiles'); + if (typeof(maxFiles)==='undefined'||maxFiles!=1){ + input.setAttribute('multiple', 'multiple'); + } else { + input.removeAttribute('multiple'); + } + if(isDirectory){ + input.setAttribute('webkitdirectory', 'webkitdirectory'); + } else { + input.removeAttribute('webkitdirectory'); + } + // When new files are added, simply append them to the overall list + input.addEventListener('change', function(e){ + appendFilesFromFileList(e.target.files,e); + e.target.value = ''; + }, false); + }); + }; + $.assignDrop = function(domNodes){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + domNode.addEventListener('dragover', onDragOver, false); + domNode.addEventListener('drop', onDrop, false); + }); + }; + $.unAssignDrop = function(domNodes) { + if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + domNode.removeEventListener('dragover', onDragOver); + domNode.removeEventListener('drop', onDrop); + }); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.files, function(file){ + if (file.isUploading()) { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.upload = function(){ + // Make sure we don't start too many uploads at once + if($.isUploading()) return; + // Kick off the queue + $.fire('uploadStart'); + for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { + $.uploadNextChunk(); + } + }; + $.pause = function(){ + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + file.abort(); + }); + $.fire('pause'); + }; + $.cancel = function(){ + for(var i = $.files.length - 1; i >= 0; i--) { + $.files[i].cancel(); + } + $.fire('cancel'); + }; + $.progress = function(){ + var totalDone = 0; + var totalSize = 0; + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + totalDone += file.progress()*file.size; + totalSize += file.size; + }); + return(totalSize>0 ? totalDone/totalSize : 0); + }; + $.addFile = function(file, event){ + appendFilesFromFileList([file], event); + }; + $.removeFile = function(file){ + for(var i = $.files.length - 1; i >= 0; i--) { + if($.files[i] === file) { + $.files.splice(i, 1); + } + } + }; + $.getFromUniqueIdentifier = function(uniqueIdentifier){ + var ret = false; + $h.each($.files, function(f){ + if(f.uniqueIdentifier==uniqueIdentifier) ret = f; + }); + return(ret); + }; + $.getSize = function(){ + var totalSize = 0; + $h.each($.files, function(file){ + totalSize += file.size; + }); + return(totalSize); + }; + + return(this); + }; + + + // Node.js-style export for Node and Component + if (typeof module != 'undefined') { + module.exports = Resumable; + } else if (typeof define === "function" && define.amd) { + // AMD/requirejs: Define the module + define(function(){ + return Resumable; + }); + } else { + // Browser: Expose to window + window.Resumable = Resumable; + } + +})(); diff --git a/ui/js/src/kimchi.template_add_main.js b/ui/js/src/kimchi.template_add_main.js index 0306571..e10b357 100644 --- a/ui/js/src/kimchi.template_add_main.js +++ b/ui/js/src/kimchi.template_add_main.js @@ -390,6 +390,33 @@ kimchi.template_add_main = function() { } } }; + //1-3 upload iso + $('#iso-upload').click(function() { + kimchi.switchPage('iso-type-box', 'iso-upload-box'); + }); + + $('#iso-upload-box-back').click(function() { + kimchi.switchPage('iso-upload-box', 'iso-type-box', 'right'); + }); + + var r = new Resumable({ + target:'storagepools/ISO/storagevolumes' + }); + + r.on('fileProgress', function(file){ + console.debug(file); + var element=document.getElementById("upload"); + var progress = Math.round(file.progress()*100)+"%" + element.innerHTML=file.fileName+ "-" + progress; + var tmp=document.getElementById("movie"); + tmp.innerHTML=['<div class="uploadify-progress"><div class="uploadify-progress-bar" style="width:', progress,'"></div></div>'].join("") + }); + + r.on('fileAdded', function(file, event){ + r.upload(); + }); + + r.assignBrowse(document.getElementById('browseButton')); }; kimchi.template_check_url = function(url) { diff --git a/ui/pages/kimchi-ui.html.tmpl b/ui/pages/kimchi-ui.html.tmpl index 7bdf441..4fc10e2 100644 --- a/ui/pages/kimchi-ui.html.tmpl +++ b/ui/pages/kimchi-ui.html.tmpl @@ -38,6 +38,7 @@ <script src="$href('libs/jquery-ui.min.js')"></script> <script src="$href('libs/jquery-ui-i18n.min.js')"></script> <script src="$href('js/kimchi.min.js')"></script> +<script src="$href('js/resumable.js')"></script> <!-- This is used for detecting if the UI needs to be built --> <style type="text/css"> diff --git a/ui/pages/template-add.html.tmpl b/ui/pages/template-add.html.tmpl index 418c5e8..796beb0 100644 --- a/ui/pages/template-add.html.tmpl +++ b/ui/pages/template-add.html.tmpl @@ -41,6 +41,9 @@ <li> <a id="iso-remote" class="remote">$_("Remote ISO Image")</a> </li> + <li> + <a id="iso-upload" class="local">$_("Upload ISO Image")</a> + </li> </ul> </div> @@ -204,6 +207,16 @@ </div> </div> + <!-- 1-3--> + <div class="page" id="iso-upload-box"> + <header> + <a class="back" id="iso-upload-box-back"></a> + <h2 class="step-title">$_("Upload ISO Image")</h2> + </header> + <a href="#" id="browseButton" class="uploadify-button">Select files</a> + <div id="upload"></div> + <div id="movie"></div> + </div> </div> </div> </div> -- 1.8.3.2

On 08/21/2014 11:43 AM, lvroyce0210@gmail.com wrote:
From: ssdxiao <ssdxiao@163.com>
Upload ISO to the path /var/lib/kimchi/iso of the local disk
Haven't we agreed to use upload/download in any pool?
Signed-off-by: ssdxiao <ssdxiao@163.com> Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- contrib/kimchi.spec.fedora.in | 3 + contrib/kimchi.spec.suse.in | 3 + po/en_US.po | 2 + po/pt_BR.po | 3 + po/zh_CN.po | 3 + src/kimchi/API.json | 1 - src/kimchi/control/storagepools.py | 4 +- src/nginx.conf.in | 1 + ui/css/theme-default/upload.css | 43 ++ ui/js/resumable.js | 816 ++++++++++++++++++++++++++++++++++ ui/js/src/kimchi.template_add_main.js | 27 ++ ui/pages/kimchi-ui.html.tmpl | 1 + ui/pages/template-add.html.tmpl | 13 + 13 files changed, 917 insertions(+), 3 deletions(-) create mode 100644 ui/css/theme-default/upload.css create mode 100644 ui/js/resumable.js
diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 5766784..6be0a26 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -175,6 +175,9 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/kimchi/ui/images/theme-default/*.png %{_datadir}/kimchi/ui/images/theme-default/*.gif %{_datadir}/kimchi/ui/js/kimchi.min.js
+%{_datadir}/kimchi/ui/js/jquery-ui.js +%{_datadir}/kimchi/ui/js/jquery.min.js
Those 2 files were removed from kimchi code.
+%{_datadir}/kimchi/ui/js/resumable.js %{_datadir}/kimchi/ui/js/modernizr.custom.2.6.2.min.js %{_datadir}/kimchi/ui/js/novnc/*.js %{_datadir}/kimchi/ui/js/spice/*.js diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 1f193d0..8dd1db6 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -96,6 +96,9 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/kimchi/ui/images/theme-default/*.png %{_datadir}/kimchi/ui/images/theme-default/*.gif %{_datadir}/kimchi/ui/js/kimchi.min.js
+%{_datadir}/kimchi/ui/js/jquery-ui.js +%{_datadir}/kimchi/ui/js/jquery.min.js
The same I commented above.
+%{_datadir}/kimchi/ui/js/resumable.js %{_datadir}/kimchi/ui/js/modernizr.custom.2.6.2.min.js %{_datadir}/kimchi/ui/js/novnc/*.js %{_datadir}/kimchi/ui/js/spice/*.js diff --git a/po/en_US.po b/po/en_US.po index a34da3a..5f6de5b 100644 --- a/po/en_US.po +++ b/po/en_US.po @@ -1864,3 +1864,5 @@ msgstr "Clone"
#~ msgid "Failed." #~ msgstr "Failed." +msgid "Upload ISO Image" +msgstr "Upload ISO Image"
I think we can use "Upload file" as it can be used for any type of file
diff --git a/po/pt_BR.po b/po/pt_BR.po index 452e778..a2b4aa7 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1928,3 +1928,6 @@ msgstr "Clonar"
#~ msgid "Failed." #~ msgstr "Falhou." + +msgid "Upload ISO Image" +msgstr "Carregar Imagem ISO" diff --git a/po/zh_CN.po b/po/zh_CN.po index 83c7018..6640032 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -1794,3 +1794,6 @@ msgstr "制作副本"
#~ msgid "Failed." #~ msgstr "失败" + +msgid "Upload ISO Image" +msgstr "上传ISO镜像" diff --git a/src/kimchi/API.json b/src/kimchi/API.json index c3fc5e3..520b1d2 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -162,7 +162,6 @@ "description": "The name of the Storage Volume", "type": "string", "minLength": 1, - "required": true, "error": "KCHVOL0013E" }, "allocation": { diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index 460beb1..16abc85 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -18,8 +18,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import cherrypy - - +import os +import errno from kimchi.control.base import Collection, Resource from kimchi.control.storagevolumes import IsoVolumes, StorageVolumes from kimchi.control.utils import get_class_name, model_fn diff --git a/src/nginx.conf.in b/src/nginx.conf.in index 1d1a398..cd54ddc 100644 --- a/src/nginx.conf.in +++ b/src/nginx.conf.in @@ -37,6 +37,7 @@ http {
access_log /var/log/nginx/access.log main; sendfile on; + client_max_body_size 2m;
# Timeout set to 10 minutes to avoid the 504 Gateway Timeout # when Kimchi is processing a request. diff --git a/ui/css/theme-default/upload.css b/ui/css/theme-default/upload.css new file mode 100644 index 0000000..9cdfe4f --- /dev/null +++ b/ui/css/theme-default/upload.css
I suggest to create a specific dir to include it. ui/css/theme-default is used for Kimchi file as this one is imported it would be good to get it in a specific location. I also would suggest to have a specific commit for imported file that way we can easily review the real code.
@@ -0,0 +1,43 @@ +/* +Uploadify +Copyright (c) 2012 Reactive Apps, Ronnie Garcia +Released under the MIT License <http://www.opensource.org/licenses/mit-license.php> +*/ + +.uploadify-button { + background-color: #505050; + background-image: linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -o-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -moz-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -webkit-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -ms-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0, #505050), + color-stop(1, #707070) + ); + background-position: center top; + background-repeat: no-repeat; + -webkit-border-radius: 30px; + -moz-border-radius: 30px; + border-radius: 30px; + border: 2px solid #808080; + color: #FFF; + height: 30px; + width: 120px; + font: bold 12px Arial, Helvetica, sans-serif; + text-align: center; + text-shadow: 0 -1px 0 rgba(0,0,0,0.25); +} +.uploadify-progress { + background-color: #E5E5E5; + margin-top: 10px; + width: 100%; +} +.uploadify-progress-bar { + background-color: #0099FF; + height: 3px; + width: 1px; +}
diff --git a/ui/js/resumable.js b/ui/js/resumable.js new file mode 100644 index 0000000..add21ec --- /dev/null +++ b/ui/js/resumable.js @@ -0,0 +1,816 @@
Same I commented above.
+/* +* MIT Licensed +* http://www.23developer.com/opensource +* http://github.com/23/resumable.js +* Steffen Tiedemann Christensen, steffen@23company.com +*/ + +(function(){ +"use strict"; + + var Resumable = function(opts){ + if ( !(this instanceof Resumable) ) { + return new Resumable(opts); + } + this.version = 1.0; + // SUPPORTED BY BROWSER? + // Check if these features are support by the browser: + // - File object type + // - Blob object type + // - FileList object type + // - slicing files + this.support = ( + (typeof(File)!=='undefined') + && + (typeof(Blob)!=='undefined') + && + (typeof(FileList)!=='undefined') + && + (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false) + ); + if(!this.support) return(false); + + + // PROPERTIES + var $ = this; + $.files = []; + $.defaults = { + chunkSize:1*1024*1024, + forceChunkSize:false, + simultaneousUploads:3, + fileParameterName:'file', + throttleProgressCallbacks:0.5, + query:{}, + headers:{}, + preprocess:null, + method:'multipart', + prioritizeFirstAndLastChunk:false, + target:'/', + testChunks:true, + generateUniqueIdentifier:null, + maxChunkRetries:undefined, + chunkRetryInterval:undefined, + permanentErrors:[404, 415, 500, 501], + maxFiles:undefined, + withCredentials:false, + xhrTimeout:0, + maxFilesErrorCallback:function (files, errorCount) { + var maxFiles = $.getOpt('maxFiles'); + alert('Please upload ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.'); + }, + minFileSize:1, + minFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.'); + }, + maxFileSize:undefined, + maxFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.'); + }, + fileType: [], + fileTypeErrorCallback: function(file, errorCount) { + alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.'); + } + }; + $.opts = opts||{}; + $.getOpt = function(o) { + var $opt = this; + // Get multiple option if passed an array + if(o instanceof Array) { + var options = {}; + $h.each(o, function(option){ + options[option] = $opt.getOpt(option); + }); + return options; + } + // Otherwise, just return a simple option + if ($opt instanceof ResumableChunk) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.fileObj; } + } + if ($opt instanceof ResumableFile) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.resumableObj; } + } + if ($opt instanceof Resumable) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { return $opt.defaults[o]; } + } + }; + + // EVENTS + // catchAll(event, ...) + // fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message), + // complete(), progress(), error(message, file), pause() + $.events = []; + $.on = function(event,callback){ + $.events.push(event.toLowerCase(), callback); + }; + $.fire = function(){ + // `arguments` is an object, not array, in FF, so: + var args = []; + for (var i=0; i<arguments.length; i++) args.push(arguments[i]); + // Find event listeners, and support pseudo-event `catchAll` + var event = args[0].toLowerCase(); + for (var i=0; i<=$.events.length; i+=2) { + if($.events[i]==event) $.events[i+1].apply($,args.slice(1)); + if($.events[i]=='catchall') $.events[i+1].apply(null,args); + } + if(event=='fileerror') $.fire('error', args[2], args[1]); + if(event=='fileprogress') $.fire('progress'); + }; + + + // INTERNAL HELPER METHODS (handy, but ultimately not part of uploading) + var $h = { + stopEvent: function(e){ + e.stopPropagation(); + e.preventDefault(); + }, + each: function(o,callback){ + if(typeof(o.length)!=='undefined') { + for (var i=0; i<o.length; i++) { + // Array or FileList + if(callback(o[i])===false) return; + } + } else { + for (i in o) { + // Object + if(callback(i,o[i])===false) return; + } + } + }, + generateUniqueIdentifier:function(file){ + var custom = $.getOpt('generateUniqueIdentifier'); + if(typeof custom === 'function') { + return custom(file); + } + var relativePath = file.webkitRelativePath||file.fileName||file.name; // Some confusion in different versions of Firefox + var size = file.size; + return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')); + }, + contains:function(array,test) { + var result = false; + + $h.each(array, function(value) { + if (value == test) { + result = true; + return false; + } + return true; + }); + + return result; + }, + formatSize:function(size){ + if(size<1024) { + return size + ' bytes'; + } else if(size<1024*1024) { + return (size/1024.0).toFixed(0) + ' KB'; + } else if(size<1024*1024*1024) { + return (size/1024.0/1024.0).toFixed(1) + ' MB'; + } else { + return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB'; + } + }, + getTarget:function(params){ + var target = $.getOpt('target'); + if(target.indexOf('?') < 0) { + target += '?'; + } else { + target += '&'; + } + return target + params.join('&'); + } + }; + + var onDrop = function(event){ + $h.stopEvent(event); + appendFilesFromFileList(event.dataTransfer.files, event); + }; + var onDragOver = function(e) { + e.preventDefault(); + }; + + // INTERNAL METHODS (both handy and responsible for the heavy load) + var appendFilesFromFileList = function(fileList, event){ + // check for uploading too many files + var errorCount = 0; + var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']); + if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) { + // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file + if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) { + $.removeFile($.files[0]); + } else { + o.maxFilesErrorCallback(fileList, errorCount++); + return false; + } + } + var files = []; + $h.each(fileList, function(file){ + var fileName = file.name.split('.'); + var fileType = fileName[fileName.length-1].toLowerCase(); + + if (o.fileType.length > 0 && !$h.contains(o.fileType, fileType)) { + o.fileTypeErrorCallback(file, errorCount++); + return false; + } + + if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) { + o.minFileSizeErrorCallback(file, errorCount++); + return false; + } + if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) { + o.maxFileSizeErrorCallback(file, errorCount++); + return false; + } + + // directories have size == 0 + if (!$.getFromUniqueIdentifier($h.generateUniqueIdentifier(file))) {(function(){ + var f = new ResumableFile($, file); + window.setTimeout(function(){ + $.files.push(f); + files.push(f); + f.container = (typeof event != 'undefined' ? event.srcElement : null); + $.fire('fileAdded', f, event) + },0); + })()}; + }); + window.setTimeout(function(){ + $.fire('filesAdded', files) + },0); + }; + + // INTERNAL OBJECT TYPES + function ResumableFile(resumableObj, file){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $._prevProgress = 0; + $.resumableObj = resumableObj; + $.file = file; + $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox + $.size = file.size; + $.relativePath = file.webkitRelativePath || $.fileName; + $.uniqueIdentifier = $h.generateUniqueIdentifier(file); + $._pause = false; + $.container = ''; + var _error = false; + + // Callback when something happens within the chunk + var chunkEvent = function(event, message){ + // event can be 'progress', 'success', 'error' or 'retry' + switch(event){ + case 'progress': + $.resumableObj.fire('fileProgress', $); + break; + case 'error': + $.abort(); + _error = true; + $.chunks = []; + $.resumableObj.fire('fileError', $, message); + break; + case 'success': + if(_error) return; + $.resumableObj.fire('fileProgress', $); // it's at least progress + if($.isComplete()) { + $.resumableObj.fire('fileSuccess', $, message); + } + break; + case 'retry': + $.resumableObj.fire('fileRetry', $); + break; + } + }; + + // Main code to set up a file object with chunks, + // packaged to be able to handle retries if needed. + $.chunks = []; + $.abort = function(){ + // Stop current uploads + var abortCount = 0; + $h.each($.chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + abortCount++; + } + }); + if(abortCount>0) $.resumableObj.fire('fileProgress', $); + }; + $.cancel = function(){ + // Reset this file to be void + var _chunks = $.chunks; + $.chunks = []; + // Stop current uploads + $h.each(_chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + $.resumableObj.uploadNextChunk(); + } + }); + $.resumableObj.removeFile($); + $.resumableObj.fire('fileProgress', $); + }; + $.retry = function(){ + $.bootstrap(); + var firedRetry = false; + $.resumableObj.on('chunkingComplete', function(){ + if(!firedRetry) $.resumableObj.upload(); + firedRetry = true; + }); + }; + $.bootstrap = function(){ + $.abort(); + _error = false; + // Rebuild stack of chunks from file + $.chunks = []; + $._prevProgress = 0; + var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor; + var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1); + for (var offset=0; offset<maxOffset; offset++) {(function(offset){ + window.setTimeout(function(){ + $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent)); + $.resumableObj.fire('chunkingProgress',$,offset/maxOffset); + },0); + })(offset)} + window.setTimeout(function(){ + $.resumableObj.fire('chunkingComplete',$); + },0); + }; + $.progress = function(){ + if(_error) return(1); + // Sum up progress across everything + var ret = 0; + var error = false; + $h.each($.chunks, function(c){ + if(c.status()=='error') error = true; + ret += c.progress(true); // get chunk progress relative to entire file + }); + ret = (error ? 1 : (ret>0.999 ? 1 : ret)); + ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused + $._prevProgress = ret; + return(ret); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.chunks, function(chunk){ + if(chunk.status()=='uploading') { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.isComplete = function(){ + var outstanding = false; + $h.each($.chunks, function(chunk){ + var status = chunk.status(); + if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { + outstanding = true; + return(false); + } + }); + return(!outstanding); + }; + $.pause = function(pause){ + if(typeof(pause)==='undefined'){ + $._pause = ($._pause ? false : true); + }else{ + $._pause = pause; + } + }; + $.isPaused = function() { + return $._pause; + }; + + + // Bootstrap and return + $.resumableObj.fire('chunkingStart', $); + $.bootstrap(); + return(this); + } + + function ResumableChunk(resumableObj, fileObj, offset, callback){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $.resumableObj = resumableObj; + $.fileObj = fileObj; + $.fileObjSize = fileObj.size; + $.fileObjType = fileObj.file.type; + $.offset = offset; + $.callback = callback; + $.lastProgressCallback = (new Date); + $.tested = false; + $.retries = 0; + $.pendingRetry = false; + $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished + + // Computed properties + var chunkSize = $.getOpt('chunkSize'); + $.loaded = 0; + $.startByte = $.offset*chunkSize; + $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); + if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { + // The last chunk will be bigger than the chunk size, but less than 2*chunkSize + $.endByte = $.fileObjSize; + } + $.xhr = null; + + // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session + $.test = function(){ + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + var testHandler = function(e){ + $.tested = true; + var status = $.status(); + if(status=='success') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.send(); + } + }; + $.xhr.addEventListener('load', testHandler, false); + $.xhr.addEventListener('error', testHandler, false); + + // Add data from the query options + var params = []; + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); + }); + // Add extra data to identify chunk + params.push(['resumableChunkNumber', encodeURIComponent($.offset+1)].join('=')); + params.push(['resumableChunkSize', encodeURIComponent($.getOpt('chunkSize'))].join('=')); + params.push(['resumableCurrentChunkSize', encodeURIComponent($.endByte - $.startByte)].join('=')); + params.push(['resumableTotalSize', encodeURIComponent($.fileObjSize)].join('=')); + params.push(['resumableType', encodeURIComponent($.fileObjType)].join('=')); + params.push(['resumableIdentifier', encodeURIComponent($.fileObj.uniqueIdentifier)].join('=')); + params.push(['resumableFilename', encodeURIComponent($.fileObj.fileName)].join('=')); + params.push(['resumableRelativePath', encodeURIComponent($.fileObj.relativePath)].join('=')); + // Append the relevant chunk and send it + $.xhr.open('GET', $h.getTarget(params)); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + $h.each($.getOpt('headers'), function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + $.xhr.send(null); + }; + + $.preprocessFinished = function(){ + $.preprocessState = 2; + $.send(); + }; + + // send() uploads the actual data in a POST call + $.send = function(){ + var preprocess = $.getOpt('preprocess'); + if(typeof preprocess === 'function') { + switch($.preprocessState) { + case 0: preprocess($); $.preprocessState = 1; return; + case 1: return; + case 2: break; + } + } + if($.getOpt('testChunks') && !$.tested) { + $.test(); + return; + } + + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + // Progress + $.xhr.upload.addEventListener('progress', function(e){ + if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) { + $.callback('progress'); + $.lastProgressCallback = (new Date); + } + $.loaded=e.loaded||0; + }, false); + $.loaded = 0; + $.pendingRetry = false; + $.callback('progress'); + + // Done (either done, failed or retry) + var doneHandler = function(e){ + var status = $.status(); + if(status=='success'||status=='error') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.callback('retry', $.message()); + $.abort(); + $.retries++; + var retryInterval = $.getOpt('chunkRetryInterval'); + if(retryInterval !== undefined) { + $.pendingRetry = true; + setTimeout($.send, retryInterval); + } else { + $.send(); + } + } + }; + $.xhr.addEventListener('load', doneHandler, false); + $.xhr.addEventListener('error', doneHandler, false); + + // Set up the basic query data from Resumable + var query = { + resumableChunkNumber: $.offset+1, + resumableChunkSize: $.getOpt('chunkSize'), + resumableCurrentChunkSize: $.endByte - $.startByte, + resumableTotalSize: $.fileObjSize, + resumableType: $.fileObjType, + resumableIdentifier: $.fileObj.uniqueIdentifier, + resumableFilename: $.fileObj.fileName, + resumableRelativePath: $.fileObj.relativePath, + resumableTotalChunks: $.fileObj.chunks.length + }; + // Mix in custom data + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + query[k] = v; + }); + + var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))), + bytes = $.fileObj.file[func]($.startByte,$.endByte), + data = null, + target = $.getOpt('target'); + + if ($.getOpt('method') === 'octet') { + // Add data from the query options + data = bytes; + var params = []; + $h.each(query, function(k,v){ + params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); + }); + target = $h.getTarget(params); + } else { + // Add data from the query options + data = new FormData(); + $h.each(query, function(k,v){ + data.append(k,v); + }); + data.append($.getOpt('fileParameterName'), bytes); + } + + $.xhr.open('POST', target); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + $h.each($.getOpt('headers'), function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + $.xhr.send(data); + }; + $.abort = function(){ + // Abort and reset + if($.xhr) $.xhr.abort(); + $.xhr = null; + }; + $.status = function(){ + // Returns: 'pending', 'uploading', 'success', 'error' + if($.pendingRetry) { + // if pending retry then that's effectively the same as actively uploading, + // there might just be a slight delay before the retry starts + return('uploading'); + } else if(!$.xhr) { + return('pending'); + } else if($.xhr.readyState<4) { + // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening + return('uploading'); + } else { + if($.xhr.status==200) { + // HTTP 200, perfect + return('success'); + } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { + // HTTP 415/500/501, permanent error + return('error'); + } else { + // this should never happen, but we'll reset and queue a retry + // a likely case for this would be 503 service unavailable + $.abort(); + return('pending'); + } + } + }; + $.message = function(){ + return($.xhr ? $.xhr.responseText : ''); + }; + $.progress = function(relative){ + if(typeof(relative)==='undefined') relative = false; + var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); + if($.pendingRetry) return(0); + var s = $.status(); + switch(s){ + case 'success': + case 'error': + return(1*factor); + case 'pending': + return(0*factor); + default: + return($.loaded/($.endByte-$.startByte)*factor); + } + }; + return(this); + } + + // QUEUE + $.uploadNextChunk = function(){ + var found = false; + + // In some cases (such as videos) it's really handy to upload the first + // and last chunk of a file quickly; this let's the server check the file's + // metadata and determine if there's even a point in continuing. + if ($.getOpt('prioritizeFirstAndLastChunk')) { + $h.each($.files, function(file){ + if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) { + file.chunks[0].send(); + found = true; + return(false); + } + if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) { + file.chunks[file.chunks.length-1].send(); + found = true; + return(false); + } + }); + if(found) return(true); + } + + // Now, simply look for the next, best thing to upload + $h.each($.files, function(file){ + if(file.isPaused()===false){ + $h.each(file.chunks, function(chunk){ + if(chunk.status()=='pending' && chunk.preprocessState === 0) { + chunk.send(); + found = true; + return(false); + } + }); + } + if(found) return(false); + }); + if(found) return(true); + + // The are no more outstanding chunks to upload, check is everything is done + var outstanding = false; + $h.each($.files, function(file){ + if(!file.isComplete()) { + outstanding = true; + return(false); + } + }); + if(!outstanding) { + // All chunks have been uploaded, complete + $.fire('complete'); + } + return(false); + }; + + + // PUBLIC METHODS FOR RESUMABLE.JS + $.assignBrowse = function(domNodes, isDirectory){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + var input; + if(domNode.tagName==='INPUT' && domNode.type==='file'){ + input = domNode; + } else { + input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.style.display = 'none'; + domNode.addEventListener('click', function(){ + input.style.opacity = 0; + input.style.display='block'; + input.focus(); + input.click(); + input.style.display='none'; + }, false); + domNode.appendChild(input); + } + var maxFiles = $.getOpt('maxFiles'); + if (typeof(maxFiles)==='undefined'||maxFiles!=1){ + input.setAttribute('multiple', 'multiple'); + } else { + input.removeAttribute('multiple'); + } + if(isDirectory){ + input.setAttribute('webkitdirectory', 'webkitdirectory'); + } else { + input.removeAttribute('webkitdirectory'); + } + // When new files are added, simply append them to the overall list + input.addEventListener('change', function(e){ + appendFilesFromFileList(e.target.files,e); + e.target.value = ''; + }, false); + }); + }; + $.assignDrop = function(domNodes){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + domNode.addEventListener('dragover', onDragOver, false); + domNode.addEventListener('drop', onDrop, false); + }); + }; + $.unAssignDrop = function(domNodes) { + if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + domNode.removeEventListener('dragover', onDragOver); + domNode.removeEventListener('drop', onDrop); + }); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.files, function(file){ + if (file.isUploading()) { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.upload = function(){ + // Make sure we don't start too many uploads at once + if($.isUploading()) return; + // Kick off the queue + $.fire('uploadStart'); + for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { + $.uploadNextChunk(); + } + }; + $.pause = function(){ + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + file.abort(); + }); + $.fire('pause'); + }; + $.cancel = function(){ + for(var i = $.files.length - 1; i >= 0; i--) { + $.files[i].cancel(); + } + $.fire('cancel'); + }; + $.progress = function(){ + var totalDone = 0; + var totalSize = 0; + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + totalDone += file.progress()*file.size; + totalSize += file.size; + }); + return(totalSize>0 ? totalDone/totalSize : 0); + }; + $.addFile = function(file, event){ + appendFilesFromFileList([file], event); + }; + $.removeFile = function(file){ + for(var i = $.files.length - 1; i >= 0; i--) { + if($.files[i] === file) { + $.files.splice(i, 1); + } + } + }; + $.getFromUniqueIdentifier = function(uniqueIdentifier){ + var ret = false; + $h.each($.files, function(f){ + if(f.uniqueIdentifier==uniqueIdentifier) ret = f; + }); + return(ret); + }; + $.getSize = function(){ + var totalSize = 0; + $h.each($.files, function(file){ + totalSize += file.size; + }); + return(totalSize); + }; + + return(this); + }; + + + // Node.js-style export for Node and Component + if (typeof module != 'undefined') { + module.exports = Resumable; + } else if (typeof define === "function" && define.amd) { + // AMD/requirejs: Define the module + define(function(){ + return Resumable; + }); + } else { + // Browser: Expose to window + window.Resumable = Resumable; + } + +})(); diff --git a/ui/js/src/kimchi.template_add_main.js b/ui/js/src/kimchi.template_add_main.js index 0306571..e10b357 100644 --- a/ui/js/src/kimchi.template_add_main.js +++ b/ui/js/src/kimchi.template_add_main.js @@ -390,6 +390,33 @@ kimchi.template_add_main = function() { } } }; + //1-3 upload iso + $('#iso-upload').click(function() { + kimchi.switchPage('iso-type-box', 'iso-upload-box'); + }); + + $('#iso-upload-box-back').click(function() { + kimchi.switchPage('iso-upload-box', 'iso-type-box', 'right'); + }); +
We have agreed to add the upload/download function to the Storage tab (in the actions menu) So this code can be removed.
+ var r = new Resumable({ + target:'storagepools/ISO/storagevolumes' + }); + + r.on('fileProgress', function(file){ + console.debug(file); + var element=document.getElementById("upload"); + var progress = Math.round(file.progress()*100)+"%" + element.innerHTML=file.fileName+ "-" + progress; + var tmp=document.getElementById("movie"); + tmp.innerHTML=['<div class="uploadify-progress"><div class="uploadify-progress-bar" style="width:', progress,'"></div></div>'].join("") + }); + + r.on('fileAdded', function(file, event){ + r.upload(); + }); + + r.assignBrowse(document.getElementById('browseButton')); };
kimchi.template_check_url = function(url) { diff --git a/ui/pages/kimchi-ui.html.tmpl b/ui/pages/kimchi-ui.html.tmpl index 7bdf441..4fc10e2 100644 --- a/ui/pages/kimchi-ui.html.tmpl +++ b/ui/pages/kimchi-ui.html.tmpl @@ -38,6 +38,7 @@ <script src="$href('libs/jquery-ui.min.js')"></script> <script src="$href('libs/jquery-ui-i18n.min.js')"></script> <script src="$href('js/kimchi.min.js')"></script> +<script src="$href('js/resumable.js')"></script>
<!-- This is used for detecting if the UI needs to be built --> <style type="text/css"> diff --git a/ui/pages/template-add.html.tmpl b/ui/pages/template-add.html.tmpl index 418c5e8..796beb0 100644 --- a/ui/pages/template-add.html.tmpl +++ b/ui/pages/template-add.html.tmpl @@ -41,6 +41,9 @@ <li> <a id="iso-remote" class="remote">$_("Remote ISO Image")</a> </li> + <li> + <a id="iso-upload" class="local">$_("Upload ISO Image")</a> + </li> </ul> </div>
@@ -204,6 +207,16 @@ </div>
</div> + <!-- 1-3--> + <div class="page" id="iso-upload-box"> + <header> + <a class="back" id="iso-upload-box-back"></a> + <h2 class="step-title">$_("Upload ISO Image")</h2> + </header> + <a href="#" id="browseButton" class="uploadify-button">Select files</a> + <div id="upload"></div> + <div id="movie"></div> + </div> </div> </div> </div>

I think we will substitute this one with another UI, it is temporarily added for backend test, and will not be merged without refactoration. On 2014年08月22日 04:02, Aline Manera wrote:
On 08/21/2014 11:43 AM, lvroyce0210@gmail.com wrote:
From: ssdxiao <ssdxiao@163.com>
Upload ISO to the path /var/lib/kimchi/iso of the local disk
Haven't we agreed to use upload/download in any pool?
Signed-off-by: ssdxiao <ssdxiao@163.com> Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- contrib/kimchi.spec.fedora.in | 3 + contrib/kimchi.spec.suse.in | 3 + po/en_US.po | 2 + po/pt_BR.po | 3 + po/zh_CN.po | 3 + src/kimchi/API.json | 1 - src/kimchi/control/storagepools.py | 4 +- src/nginx.conf.in | 1 + ui/css/theme-default/upload.css | 43 ++ ui/js/resumable.js | 816 ++++++++++++++++++++++++++++++++++ ui/js/src/kimchi.template_add_main.js | 27 ++ ui/pages/kimchi-ui.html.tmpl | 1 + ui/pages/template-add.html.tmpl | 13 + 13 files changed, 917 insertions(+), 3 deletions(-) create mode 100644 ui/css/theme-default/upload.css create mode 100644 ui/js/resumable.js
diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 5766784..6be0a26 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -175,6 +175,9 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/kimchi/ui/images/theme-default/*.png %{_datadir}/kimchi/ui/images/theme-default/*.gif %{_datadir}/kimchi/ui/js/kimchi.min.js
+%{_datadir}/kimchi/ui/js/jquery-ui.js +%{_datadir}/kimchi/ui/js/jquery.min.js
Those 2 files were removed from kimchi code.
+%{_datadir}/kimchi/ui/js/resumable.js %{_datadir}/kimchi/ui/js/modernizr.custom.2.6.2.min.js %{_datadir}/kimchi/ui/js/novnc/*.js %{_datadir}/kimchi/ui/js/spice/*.js diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 1f193d0..8dd1db6 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -96,6 +96,9 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/kimchi/ui/images/theme-default/*.png %{_datadir}/kimchi/ui/images/theme-default/*.gif %{_datadir}/kimchi/ui/js/kimchi.min.js
+%{_datadir}/kimchi/ui/js/jquery-ui.js +%{_datadir}/kimchi/ui/js/jquery.min.js
The same I commented above.
+%{_datadir}/kimchi/ui/js/resumable.js %{_datadir}/kimchi/ui/js/modernizr.custom.2.6.2.min.js %{_datadir}/kimchi/ui/js/novnc/*.js %{_datadir}/kimchi/ui/js/spice/*.js diff --git a/po/en_US.po b/po/en_US.po index a34da3a..5f6de5b 100644 --- a/po/en_US.po +++ b/po/en_US.po @@ -1864,3 +1864,5 @@ msgstr "Clone" #~ msgid "Failed." #~ msgstr "Failed." +msgid "Upload ISO Image" +msgstr "Upload ISO Image"
I think we can use "Upload file" as it can be used for any type of file
diff --git a/po/pt_BR.po b/po/pt_BR.po index 452e778..a2b4aa7 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1928,3 +1928,6 @@ msgstr "Clonar" #~ msgid "Failed." #~ msgstr "Falhou." + +msgid "Upload ISO Image" +msgstr "Carregar Imagem ISO" diff --git a/po/zh_CN.po b/po/zh_CN.po index 83c7018..6640032 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -1794,3 +1794,6 @@ msgstr "制作副本" #~ msgid "Failed." #~ msgstr "失败" + +msgid "Upload ISO Image" +msgstr "上传ISO镜像" diff --git a/src/kimchi/API.json b/src/kimchi/API.json index c3fc5e3..520b1d2 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -162,7 +162,6 @@ "description": "The name of the Storage Volume", "type": "string", "minLength": 1, - "required": true, "error": "KCHVOL0013E" }, "allocation": { diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index 460beb1..16abc85 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -18,8 +18,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import cherrypy - - +import os +import errno from kimchi.control.base import Collection, Resource from kimchi.control.storagevolumes import IsoVolumes, StorageVolumes from kimchi.control.utils import get_class_name, model_fn diff --git a/src/nginx.conf.in b/src/nginx.conf.in index 1d1a398..cd54ddc 100644 --- a/src/nginx.conf.in +++ b/src/nginx.conf.in @@ -37,6 +37,7 @@ http { access_log /var/log/nginx/access.log main; sendfile on; + client_max_body_size 2m; # Timeout set to 10 minutes to avoid the 504 Gateway Timeout # when Kimchi is processing a request. diff --git a/ui/css/theme-default/upload.css b/ui/css/theme-default/upload.css new file mode 100644 index 0000000..9cdfe4f --- /dev/null +++ b/ui/css/theme-default/upload.css
I suggest to create a specific dir to include it.
ui/css/theme-default is used for Kimchi file as this one is imported it would be good to get it in a specific location.
I also would suggest to have a specific commit for imported file that way we can easily review the real code.
@@ -0,0 +1,43 @@ +/* +Uploadify +Copyright (c) 2012 Reactive Apps, Ronnie Garcia +Released under the MIT License <http://www.opensource.org/licenses/mit-license.php> +*/ + +.uploadify-button { + background-color: #505050; + background-image: linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -o-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -moz-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -webkit-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -ms-linear-gradient(bottom, #505050 0%, #707070 100%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0, #505050), + color-stop(1, #707070) + ); + background-position: center top; + background-repeat: no-repeat; + -webkit-border-radius: 30px; + -moz-border-radius: 30px; + border-radius: 30px; + border: 2px solid #808080; + color: #FFF; + height: 30px; + width: 120px; + font: bold 12px Arial, Helvetica, sans-serif; + text-align: center; + text-shadow: 0 -1px 0 rgba(0,0,0,0.25); +} +.uploadify-progress { + background-color: #E5E5E5; + margin-top: 10px; + width: 100%; +} +.uploadify-progress-bar { + background-color: #0099FF; + height: 3px; + width: 1px; +}
diff --git a/ui/js/resumable.js b/ui/js/resumable.js new file mode 100644 index 0000000..add21ec --- /dev/null +++ b/ui/js/resumable.js @@ -0,0 +1,816 @@
Same I commented above.
+/* +* MIT Licensed +* http://www.23developer.com/opensource +* http://github.com/23/resumable.js +* Steffen Tiedemann Christensen, steffen@23company.com +*/ + +(function(){ +"use strict"; + + var Resumable = function(opts){ + if ( !(this instanceof Resumable) ) { + return new Resumable(opts); + } + this.version = 1.0; + // SUPPORTED BY BROWSER? + // Check if these features are support by the browser: + // - File object type + // - Blob object type + // - FileList object type + // - slicing files + this.support = ( + (typeof(File)!=='undefined') + && + (typeof(Blob)!=='undefined') + && + (typeof(FileList)!=='undefined') + && + (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false) + ); + if(!this.support) return(false); + + + // PROPERTIES + var $ = this; + $.files = []; + $.defaults = { + chunkSize:1*1024*1024, + forceChunkSize:false, + simultaneousUploads:3, + fileParameterName:'file', + throttleProgressCallbacks:0.5, + query:{}, + headers:{}, + preprocess:null, + method:'multipart', + prioritizeFirstAndLastChunk:false, + target:'/', + testChunks:true, + generateUniqueIdentifier:null, + maxChunkRetries:undefined, + chunkRetryInterval:undefined, + permanentErrors:[404, 415, 500, 501], + maxFiles:undefined, + withCredentials:false, + xhrTimeout:0, + maxFilesErrorCallback:function (files, errorCount) { + var maxFiles = $.getOpt('maxFiles'); + alert('Please upload ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.'); + }, + minFileSize:1, + minFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.'); + }, + maxFileSize:undefined, + maxFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.'); + }, + fileType: [], + fileTypeErrorCallback: function(file, errorCount) { + alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.'); + } + }; + $.opts = opts||{}; + $.getOpt = function(o) { + var $opt = this; + // Get multiple option if passed an array + if(o instanceof Array) { + var options = {}; + $h.each(o, function(option){ + options[option] = $opt.getOpt(option); + }); + return options; + } + // Otherwise, just return a simple option + if ($opt instanceof ResumableChunk) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.fileObj; } + } + if ($opt instanceof ResumableFile) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.resumableObj; } + } + if ($opt instanceof Resumable) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { return $opt.defaults[o]; } + } + }; + + // EVENTS + // catchAll(event, ...) + // fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message), + // complete(), progress(), error(message, file), pause() + $.events = []; + $.on = function(event,callback){ + $.events.push(event.toLowerCase(), callback); + }; + $.fire = function(){ + // `arguments` is an object, not array, in FF, so: + var args = []; + for (var i=0; i<arguments.length; i++) args.push(arguments[i]); + // Find event listeners, and support pseudo-event `catchAll` + var event = args[0].toLowerCase(); + for (var i=0; i<=$.events.length; i+=2) { + if($.events[i]==event) $.events[i+1].apply($,args.slice(1)); + if($.events[i]=='catchall') $.events[i+1].apply(null,args); + } + if(event=='fileerror') $.fire('error', args[2], args[1]); + if(event=='fileprogress') $.fire('progress'); + }; + + + // INTERNAL HELPER METHODS (handy, but ultimately not part of uploading) + var $h = { + stopEvent: function(e){ + e.stopPropagation(); + e.preventDefault(); + }, + each: function(o,callback){ + if(typeof(o.length)!=='undefined') { + for (var i=0; i<o.length; i++) { + // Array or FileList + if(callback(o[i])===false) return; + } + } else { + for (i in o) { + // Object + if(callback(i,o[i])===false) return; + } + } + }, + generateUniqueIdentifier:function(file){ + var custom = $.getOpt('generateUniqueIdentifier'); + if(typeof custom === 'function') { + return custom(file); + } + var relativePath = file.webkitRelativePath||file.fileName||file.name; // Some confusion in different versions of Firefox + var size = file.size; + return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')); + }, + contains:function(array,test) { + var result = false; + + $h.each(array, function(value) { + if (value == test) { + result = true; + return false; + } + return true; + }); + + return result; + }, + formatSize:function(size){ + if(size<1024) { + return size + ' bytes'; + } else if(size<1024*1024) { + return (size/1024.0).toFixed(0) + ' KB'; + } else if(size<1024*1024*1024) { + return (size/1024.0/1024.0).toFixed(1) + ' MB'; + } else { + return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB'; + } + }, + getTarget:function(params){ + var target = $.getOpt('target'); + if(target.indexOf('?') < 0) { + target += '?'; + } else { + target += '&'; + } + return target + params.join('&'); + } + }; + + var onDrop = function(event){ + $h.stopEvent(event); + appendFilesFromFileList(event.dataTransfer.files, event); + }; + var onDragOver = function(e) { + e.preventDefault(); + }; + + // INTERNAL METHODS (both handy and responsible for the heavy load) + var appendFilesFromFileList = function(fileList, event){ + // check for uploading too many files + var errorCount = 0; + var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']); + if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) { + // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file + if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) { + $.removeFile($.files[0]); + } else { + o.maxFilesErrorCallback(fileList, errorCount++); + return false; + } + } + var files = []; + $h.each(fileList, function(file){ + var fileName = file.name.split('.'); + var fileType = fileName[fileName.length-1].toLowerCase(); + + if (o.fileType.length > 0 && !$h.contains(o.fileType, fileType)) { + o.fileTypeErrorCallback(file, errorCount++); + return false; + } + + if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) { + o.minFileSizeErrorCallback(file, errorCount++); + return false; + } + if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) { + o.maxFileSizeErrorCallback(file, errorCount++); + return false; + } + + // directories have size == 0 + if (!$.getFromUniqueIdentifier($h.generateUniqueIdentifier(file))) {(function(){ + var f = new ResumableFile($, file); + window.setTimeout(function(){ + $.files.push(f); + files.push(f); + f.container = (typeof event != 'undefined' ? event.srcElement : null); + $.fire('fileAdded', f, event) + },0); + })()}; + }); + window.setTimeout(function(){ + $.fire('filesAdded', files) + },0); + }; + + // INTERNAL OBJECT TYPES + function ResumableFile(resumableObj, file){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $._prevProgress = 0; + $.resumableObj = resumableObj; + $.file = file; + $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox + $.size = file.size; + $.relativePath = file.webkitRelativePath || $.fileName; + $.uniqueIdentifier = $h.generateUniqueIdentifier(file); + $._pause = false; + $.container = ''; + var _error = false; + + // Callback when something happens within the chunk + var chunkEvent = function(event, message){ + // event can be 'progress', 'success', 'error' or 'retry' + switch(event){ + case 'progress': + $.resumableObj.fire('fileProgress', $); + break; + case 'error': + $.abort(); + _error = true; + $.chunks = []; + $.resumableObj.fire('fileError', $, message); + break; + case 'success': + if(_error) return; + $.resumableObj.fire('fileProgress', $); // it's at least progress + if($.isComplete()) { + $.resumableObj.fire('fileSuccess', $, message); + } + break; + case 'retry': + $.resumableObj.fire('fileRetry', $); + break; + } + }; + + // Main code to set up a file object with chunks, + // packaged to be able to handle retries if needed. + $.chunks = []; + $.abort = function(){ + // Stop current uploads + var abortCount = 0; + $h.each($.chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + abortCount++; + } + }); + if(abortCount>0) $.resumableObj.fire('fileProgress', $); + }; + $.cancel = function(){ + // Reset this file to be void + var _chunks = $.chunks; + $.chunks = []; + // Stop current uploads + $h.each(_chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + $.resumableObj.uploadNextChunk(); + } + }); + $.resumableObj.removeFile($); + $.resumableObj.fire('fileProgress', $); + }; + $.retry = function(){ + $.bootstrap(); + var firedRetry = false; + $.resumableObj.on('chunkingComplete', function(){ + if(!firedRetry) $.resumableObj.upload(); + firedRetry = true; + }); + }; + $.bootstrap = function(){ + $.abort(); + _error = false; + // Rebuild stack of chunks from file + $.chunks = []; + $._prevProgress = 0; + var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor; + var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1); + for (var offset=0; offset<maxOffset; offset++) {(function(offset){ + window.setTimeout(function(){ + $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent)); + $.resumableObj.fire('chunkingProgress',$,offset/maxOffset); + },0); + })(offset)} + window.setTimeout(function(){ + $.resumableObj.fire('chunkingComplete',$); + },0); + }; + $.progress = function(){ + if(_error) return(1); + // Sum up progress across everything + var ret = 0; + var error = false; + $h.each($.chunks, function(c){ + if(c.status()=='error') error = true; + ret += c.progress(true); // get chunk progress relative to entire file + }); + ret = (error ? 1 : (ret>0.999 ? 1 : ret)); + ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused + $._prevProgress = ret; + return(ret); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.chunks, function(chunk){ + if(chunk.status()=='uploading') { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.isComplete = function(){ + var outstanding = false; + $h.each($.chunks, function(chunk){ + var status = chunk.status(); + if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { + outstanding = true; + return(false); + } + }); + return(!outstanding); + }; + $.pause = function(pause){ + if(typeof(pause)==='undefined'){ + $._pause = ($._pause ? false : true); + }else{ + $._pause = pause; + } + }; + $.isPaused = function() { + return $._pause; + }; + + + // Bootstrap and return + $.resumableObj.fire('chunkingStart', $); + $.bootstrap(); + return(this); + } + + function ResumableChunk(resumableObj, fileObj, offset, callback){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $.resumableObj = resumableObj; + $.fileObj = fileObj; + $.fileObjSize = fileObj.size; + $.fileObjType = fileObj.file.type; + $.offset = offset; + $.callback = callback; + $.lastProgressCallback = (new Date); + $.tested = false; + $.retries = 0; + $.pendingRetry = false; + $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished + + // Computed properties + var chunkSize = $.getOpt('chunkSize'); + $.loaded = 0; + $.startByte = $.offset*chunkSize; + $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); + if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { + // The last chunk will be bigger than the chunk size, but less than 2*chunkSize + $.endByte = $.fileObjSize; + } + $.xhr = null; + + // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session + $.test = function(){ + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + var testHandler = function(e){ + $.tested = true; + var status = $.status(); + if(status=='success') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.send(); + } + }; + $.xhr.addEventListener('load', testHandler, false); + $.xhr.addEventListener('error', testHandler, false); + + // Add data from the query options + var params = []; + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); + }); + // Add extra data to identify chunk + params.push(['resumableChunkNumber', encodeURIComponent($.offset+1)].join('=')); + params.push(['resumableChunkSize', encodeURIComponent($.getOpt('chunkSize'))].join('=')); + params.push(['resumableCurrentChunkSize', encodeURIComponent($.endByte - $.startByte)].join('=')); + params.push(['resumableTotalSize', encodeURIComponent($.fileObjSize)].join('=')); + params.push(['resumableType', encodeURIComponent($.fileObjType)].join('=')); + params.push(['resumableIdentifier', encodeURIComponent($.fileObj.uniqueIdentifier)].join('=')); + params.push(['resumableFilename', encodeURIComponent($.fileObj.fileName)].join('=')); + params.push(['resumableRelativePath', encodeURIComponent($.fileObj.relativePath)].join('=')); + // Append the relevant chunk and send it + $.xhr.open('GET', $h.getTarget(params)); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + $h.each($.getOpt('headers'), function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + $.xhr.send(null); + }; + + $.preprocessFinished = function(){ + $.preprocessState = 2; + $.send(); + }; + + // send() uploads the actual data in a POST call + $.send = function(){ + var preprocess = $.getOpt('preprocess'); + if(typeof preprocess === 'function') { + switch($.preprocessState) { + case 0: preprocess($); $.preprocessState = 1; return; + case 1: return; + case 2: break; + } + } + if($.getOpt('testChunks') && !$.tested) { + $.test(); + return; + } + + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + // Progress + $.xhr.upload.addEventListener('progress', function(e){ + if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) { + $.callback('progress'); + $.lastProgressCallback = (new Date); + } + $.loaded=e.loaded||0; + }, false); + $.loaded = 0; + $.pendingRetry = false; + $.callback('progress'); + + // Done (either done, failed or retry) + var doneHandler = function(e){ + var status = $.status(); + if(status=='success'||status=='error') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.callback('retry', $.message()); + $.abort(); + $.retries++; + var retryInterval = $.getOpt('chunkRetryInterval'); + if(retryInterval !== undefined) { + $.pendingRetry = true; + setTimeout($.send, retryInterval); + } else { + $.send(); + } + } + }; + $.xhr.addEventListener('load', doneHandler, false); + $.xhr.addEventListener('error', doneHandler, false); + + // Set up the basic query data from Resumable + var query = { + resumableChunkNumber: $.offset+1, + resumableChunkSize: $.getOpt('chunkSize'), + resumableCurrentChunkSize: $.endByte - $.startByte, + resumableTotalSize: $.fileObjSize, + resumableType: $.fileObjType, + resumableIdentifier: $.fileObj.uniqueIdentifier, + resumableFilename: $.fileObj.fileName, + resumableRelativePath: $.fileObj.relativePath, + resumableTotalChunks: $.fileObj.chunks.length + }; + // Mix in custom data + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + query[k] = v; + }); + + var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))), + bytes = $.fileObj.file[func]($.startByte,$.endByte), + data = null, + target = $.getOpt('target'); + + if ($.getOpt('method') === 'octet') { + // Add data from the query options + data = bytes; + var params = []; + $h.each(query, function(k,v){ + params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); + }); + target = $h.getTarget(params); + } else { + // Add data from the query options + data = new FormData(); + $h.each(query, function(k,v){ + data.append(k,v); + }); + data.append($.getOpt('fileParameterName'), bytes); + } + + $.xhr.open('POST', target); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + $h.each($.getOpt('headers'), function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + $.xhr.send(data); + }; + $.abort = function(){ + // Abort and reset + if($.xhr) $.xhr.abort(); + $.xhr = null; + }; + $.status = function(){ + // Returns: 'pending', 'uploading', 'success', 'error' + if($.pendingRetry) { + // if pending retry then that's effectively the same as actively uploading, + // there might just be a slight delay before the retry starts + return('uploading'); + } else if(!$.xhr) { + return('pending'); + } else if($.xhr.readyState<4) { + // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening + return('uploading'); + } else { + if($.xhr.status==200) { + // HTTP 200, perfect + return('success'); + } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { + // HTTP 415/500/501, permanent error + return('error'); + } else { + // this should never happen, but we'll reset and queue a retry + // a likely case for this would be 503 service unavailable + $.abort(); + return('pending'); + } + } + }; + $.message = function(){ + return($.xhr ? $.xhr.responseText : ''); + }; + $.progress = function(relative){ + if(typeof(relative)==='undefined') relative = false; + var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); + if($.pendingRetry) return(0); + var s = $.status(); + switch(s){ + case 'success': + case 'error': + return(1*factor); + case 'pending': + return(0*factor); + default: + return($.loaded/($.endByte-$.startByte)*factor); + } + }; + return(this); + } + + // QUEUE + $.uploadNextChunk = function(){ + var found = false; + + // In some cases (such as videos) it's really handy to upload the first + // and last chunk of a file quickly; this let's the server check the file's + // metadata and determine if there's even a point in continuing. + if ($.getOpt('prioritizeFirstAndLastChunk')) { + $h.each($.files, function(file){ + if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) { + file.chunks[0].send(); + found = true; + return(false); + } + if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) { + file.chunks[file.chunks.length-1].send(); + found = true; + return(false); + } + }); + if(found) return(true); + } + + // Now, simply look for the next, best thing to upload + $h.each($.files, function(file){ + if(file.isPaused()===false){ + $h.each(file.chunks, function(chunk){ + if(chunk.status()=='pending' && chunk.preprocessState === 0) { + chunk.send(); + found = true; + return(false); + } + }); + } + if(found) return(false); + }); + if(found) return(true); + + // The are no more outstanding chunks to upload, check is everything is done + var outstanding = false; + $h.each($.files, function(file){ + if(!file.isComplete()) { + outstanding = true; + return(false); + } + }); + if(!outstanding) { + // All chunks have been uploaded, complete + $.fire('complete'); + } + return(false); + }; + + + // PUBLIC METHODS FOR RESUMABLE.JS + $.assignBrowse = function(domNodes, isDirectory){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + var input; + if(domNode.tagName==='INPUT' && domNode.type==='file'){ + input = domNode; + } else { + input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.style.display = 'none'; + domNode.addEventListener('click', function(){ + input.style.opacity = 0; + input.style.display='block'; + input.focus(); + input.click(); + input.style.display='none'; + }, false); + domNode.appendChild(input); + } + var maxFiles = $.getOpt('maxFiles'); + if (typeof(maxFiles)==='undefined'||maxFiles!=1){ + input.setAttribute('multiple', 'multiple'); + } else { + input.removeAttribute('multiple'); + } + if(isDirectory){ + input.setAttribute('webkitdirectory', 'webkitdirectory'); + } else { + input.removeAttribute('webkitdirectory'); + } + // When new files are added, simply append them to the overall list + input.addEventListener('change', function(e){ + appendFilesFromFileList(e.target.files,e); + e.target.value = ''; + }, false); + }); + }; + $.assignDrop = function(domNodes){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + domNode.addEventListener('dragover', onDragOver, false); + domNode.addEventListener('drop', onDrop, false); + }); + }; + $.unAssignDrop = function(domNodes) { + if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + domNode.removeEventListener('dragover', onDragOver); + domNode.removeEventListener('drop', onDrop); + }); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.files, function(file){ + if (file.isUploading()) { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.upload = function(){ + // Make sure we don't start too many uploads at once + if($.isUploading()) return; + // Kick off the queue + $.fire('uploadStart'); + for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { + $.uploadNextChunk(); + } + }; + $.pause = function(){ + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + file.abort(); + }); + $.fire('pause'); + }; + $.cancel = function(){ + for(var i = $.files.length - 1; i >= 0; i--) { + $.files[i].cancel(); + } + $.fire('cancel'); + }; + $.progress = function(){ + var totalDone = 0; + var totalSize = 0; + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + totalDone += file.progress()*file.size; + totalSize += file.size; + }); + return(totalSize>0 ? totalDone/totalSize : 0); + }; + $.addFile = function(file, event){ + appendFilesFromFileList([file], event); + }; + $.removeFile = function(file){ + for(var i = $.files.length - 1; i >= 0; i--) { + if($.files[i] === file) { + $.files.splice(i, 1); + } + } + }; + $.getFromUniqueIdentifier = function(uniqueIdentifier){ + var ret = false; + $h.each($.files, function(f){ + if(f.uniqueIdentifier==uniqueIdentifier) ret = f; + }); + return(ret); + }; + $.getSize = function(){ + var totalSize = 0; + $h.each($.files, function(file){ + totalSize += file.size; + }); + return(totalSize); + }; + + return(this); + }; + + + // Node.js-style export for Node and Component + if (typeof module != 'undefined') { + module.exports = Resumable; + } else if (typeof define === "function" && define.amd) { + // AMD/requirejs: Define the module + define(function(){ + return Resumable; + }); + } else { + // Browser: Expose to window + window.Resumable = Resumable; + } + +})(); diff --git a/ui/js/src/kimchi.template_add_main.js b/ui/js/src/kimchi.template_add_main.js index 0306571..e10b357 100644 --- a/ui/js/src/kimchi.template_add_main.js +++ b/ui/js/src/kimchi.template_add_main.js @@ -390,6 +390,33 @@ kimchi.template_add_main = function() { } } }; + //1-3 upload iso + $('#iso-upload').click(function() { + kimchi.switchPage('iso-type-box', 'iso-upload-box'); + }); + + $('#iso-upload-box-back').click(function() { + kimchi.switchPage('iso-upload-box', 'iso-type-box', 'right'); + }); +
We have agreed to add the upload/download function to the Storage tab (in the actions menu) So this code can be removed.
+ var r = new Resumable({ + target:'storagepools/ISO/storagevolumes' + }); + + r.on('fileProgress', function(file){ + console.debug(file); + var element=document.getElementById("upload"); + var progress = Math.round(file.progress()*100)+"%" + element.innerHTML=file.fileName+ "-" + progress; + var tmp=document.getElementById("movie"); + tmp.innerHTML=['<div class="uploadify-progress"><div class="uploadify-progress-bar" style="width:', progress,'"></div></div>'].join("") + }); + + r.on('fileAdded', function(file, event){ + r.upload(); + }); + + r.assignBrowse(document.getElementById('browseButton')); }; kimchi.template_check_url = function(url) { diff --git a/ui/pages/kimchi-ui.html.tmpl b/ui/pages/kimchi-ui.html.tmpl index 7bdf441..4fc10e2 100644 --- a/ui/pages/kimchi-ui.html.tmpl +++ b/ui/pages/kimchi-ui.html.tmpl @@ -38,6 +38,7 @@ <script src="$href('libs/jquery-ui.min.js')"></script> <script src="$href('libs/jquery-ui-i18n.min.js')"></script> <script src="$href('js/kimchi.min.js')"></script> +<script src="$href('js/resumable.js')"></script> <!-- This is used for detecting if the UI needs to be built --> <style type="text/css"> diff --git a/ui/pages/template-add.html.tmpl b/ui/pages/template-add.html.tmpl index 418c5e8..796beb0 100644 --- a/ui/pages/template-add.html.tmpl +++ b/ui/pages/template-add.html.tmpl @@ -41,6 +41,9 @@ <li> <a id="iso-remote" class="remote">$_("Remote ISO Image")</a> </li> + <li> + <a id="iso-upload" class="local">$_("Upload ISO Image")</a> + </li> </ul> </div> @@ -204,6 +207,16 @@ </div> </div> + <!-- 1-3--> + <div class="page" id="iso-upload-box"> + <header> + <a class="back" id="iso-upload-box-back"></a> + <h2 class="step-title">$_("Upload ISO Image")</h2> + </header> + <a href="#" id="browseButton" class="uploadify-button">Select files</a> + <div id="upload"></div> + <div id="movie"></div> + </div> </div> </div> </div>
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

On 08/21/2014 11:42 AM, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Note: The UI patch is just used for testing.
Problem remains: 1. POST return 404 because the UI patch does not include accept json 2. task id does not returned in vol data and progress has not been added
I can use the "message" in the task resource to return the volume name. That way we can display the UI with a loading icon (instead of the progress bar) as is the option 2 on mock up ([Kimchi-devel] [RFC] UI Mockup - ISO Pool: Download from URL and Upload)
3. Haven't tested large file, so not sure whether we need to split large file into peices and call POST multi-times.
Royce Lv (3): Storage volume upload: Dispatch volume create to right handler Storage volume upload: Parse params for upload formdata Storage volume upload: add model function of upload
ssdxiao (1): Support to upload ISO
contrib/kimchi.spec.fedora.in | 3 + contrib/kimchi.spec.suse.in | 3 + po/en_US.po | 2 + po/pt_BR.po | 3 + po/zh_CN.po | 3 + src/kimchi/API.json | 1 - src/kimchi/control/storagepools.py | 4 +- src/kimchi/control/utils.py | 2 + src/kimchi/i18n.py | 1 + src/kimchi/model/storagevolumes.py | 34 ++ 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 + 16 files changed, 954 insertions(+), 3 deletions(-) create mode 100644 ui/css/theme-default/upload.css create mode 100644 ui/js/resumable.js

On 2014年08月22日 04:04, Aline Manera wrote:
On 08/21/2014 11:42 AM, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Note: The UI patch is just used for testing.
Problem remains: 1. POST return 404 because the UI patch does not include accept json 2. task id does not returned in vol data and progress has not been added
I can use the "message" in the task resource to return the volume name. That way we can display the UI with a loading icon (instead of the progress bar) as is the option 2 on mock up ([Kimchi-devel] [RFC] UI Mockup - ISO Pool: Download from URL and Upload)
I want to discuss whether we would like to adopt Xiao Ding's resumable upload or upload at a time. If we split file into pieces (just as Ding did in his patch), we will call POST /storagevolumes for multi times. Advantage is we can resume upload after cancel it, because we tracked split block information. But the API seems improper. Meanwhile, we can adopt my way to transfer file all at a time. We can't have the advantage of resume and pause, but the API definition seems keep align with the implementation.
3. Haven't tested large file, so not sure whether we need to split large file into peices and call POST multi-times.
Royce Lv (3): Storage volume upload: Dispatch volume create to right handler Storage volume upload: Parse params for upload formdata Storage volume upload: add model function of upload
ssdxiao (1): Support to upload ISO
contrib/kimchi.spec.fedora.in | 3 + contrib/kimchi.spec.suse.in | 3 + po/en_US.po | 2 + po/pt_BR.po | 3 + po/zh_CN.po | 3 + src/kimchi/API.json | 1 - src/kimchi/control/storagepools.py | 4 +- src/kimchi/control/utils.py | 2 + src/kimchi/i18n.py | 1 + src/kimchi/model/storagevolumes.py | 34 ++ 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 + 16 files changed, 954 insertions(+), 3 deletions(-) create mode 100644 ui/css/theme-default/upload.css create mode 100644 ui/js/resumable.js
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

On 08/25/2014 05:04 AM, Royce Lv wrote:
On 2014年08月22日 04:04, Aline Manera wrote:
On 08/21/2014 11:42 AM, lvroyce0210@gmail.com wrote:
From: Royce Lv <lvroyce@linux.vnet.ibm.com>
Note: The UI patch is just used for testing.
Problem remains: 1. POST return 404 because the UI patch does not include accept json 2. task id does not returned in vol data and progress has not been added
I can use the "message" in the task resource to return the volume name. That way we can display the UI with a loading icon (instead of the progress bar) as is the option 2 on mock up ([Kimchi-devel] [RFC] UI Mockup - ISO Pool: Download from URL and Upload)
I want to discuss whether we would like to adopt Xiao Ding's resumable upload or upload at a time. If we split file into pieces (just as Ding did in his patch), we will call POST /storagevolumes for multi times. Advantage is we can resume upload after cancel it, because we tracked split block information. But the API seems improper.
Meanwhile, we can adopt my way to transfer file all at a time. We can't have the advantage of resume and pause, but the API definition seems keep align with the implementation.
Yeap. I don't think have pause/resume is critical by now.
3. Haven't tested large file, so not sure whether we need to split large file into peices and call POST multi-times.
Royce Lv (3): Storage volume upload: Dispatch volume create to right handler Storage volume upload: Parse params for upload formdata Storage volume upload: add model function of upload
ssdxiao (1): Support to upload ISO
contrib/kimchi.spec.fedora.in | 3 + contrib/kimchi.spec.suse.in | 3 + po/en_US.po | 2 + po/pt_BR.po | 3 + po/zh_CN.po | 3 + src/kimchi/API.json | 1 - src/kimchi/control/storagepools.py | 4 +- src/kimchi/control/utils.py | 2 + src/kimchi/i18n.py | 1 + src/kimchi/model/storagevolumes.py | 34 ++ 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 + 16 files changed, 954 insertions(+), 3 deletions(-) create mode 100644 ui/css/theme-default/upload.css create mode 100644 ui/js/resumable.js
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel
participants (4)
-
Aline Manera
-
Crístian Viana
-
lvroyce0210@gmail.com
-
Royce Lv