[pve-devel] [RFC manager] fix #3248: GUI: storage: upload multiple files

Matthias Heiserer m.heiserer at proxmox.com
Wed Jun 29 14:23:22 CEST 2022


Allows queueing multiple files for upload to the storage, which wasn't
possible with the old upload window.

Signed-off-by: Matthias Heiserer <m.heiserer at proxmox.com>
---
 www/manager6/window/UploadToStorage.js | 393 +++++++++++++------------
 1 file changed, 210 insertions(+), 183 deletions(-)

diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
index 0de6d89d..bf656164 100644
--- a/www/manager6/window/UploadToStorage.js
+++ b/www/manager6/window/UploadToStorage.js
@@ -1,3 +1,13 @@
+Ext.define('pve-multiupload', {
+	extend: 'Ext.data.Model',
+	fields: [
+	    'file', 'filename', 'progressWidget', 'hashsum', 'hashWidget',
+	    'xhr', 'mimetype', 'size',
+	    {
+		name: 'hash', defaultValue: '__default__',
+	    },
+	],
+});
 Ext.define('PVE.window.UploadToStorage', {
     extend: 'Ext.window.Window',
     alias: 'widget.pveStorageUpload',
@@ -27,93 +37,102 @@ Ext.define('PVE.window.UploadToStorage', {
 
     viewModel: {
 	data: {
-	    size: '-',
-	    mimetype: '-',
-	    filename: '',
+	    hasFiles: false,
+	    uploadInProgress: false,
 	},
     },
-
     controller: {
-	submit: function(button) {
-	    const view = this.getView();
-	    const form = this.lookup('formPanel').getForm();
-	    const abortBtn = this.lookup('abortBtn');
-	    const pbar = this.lookup('progressBar');
-
-	    const updateProgress = function(per, bytes) {
-		let text = (per * 100).toFixed(2) + '%';
-		if (bytes) {
-		    text += " (" + Proxmox.Utils.format_size(bytes) + ')';
-		}
-		pbar.updateProgress(per, text);
-	    };
 
+	addFile: function(input) {
+	    let me = this;
+	    let grid = me.lookup('grid');
+	    for (const file of input.fileInputEl.dom.files) {
+		grid.store.add({
+		    file: file,
+		    filename: file.name,
+		    mimetype: Proxmox.Utils.format_size(file.size),
+		    size: file.type,
+		});
+	    }
+	},
+
+	currentUploadIndex: 1,
+	startUpload: function() {
+	    const me = this;
+	    const view = me.getView();
+	    const grid = me.lookup('grid');
+	    view.taskDone();
+
+	    const last = grid.store.last();
+	    if (!last) {
+		me.getViewModel().set('uploadInProgress', false);
+		return;
+	    }
+	    const endId = parseInt(last.id.replace('pve-multiupload-', ''), 10);
+	    let record;
+	    while (!record && me.currentUploadIndex <= endId) {
+		record = grid.store.getById(`pve-multiupload-${me.currentUploadIndex++}`);
+	    }
+
+	    if (!record) {
+		me.getViewModel().set('uploadInProgress', false);
+		return;
+	    }
+
+	    const data = record.data;
 	    const fd = new FormData();
-
-	    button.setDisabled(true);
-	    abortBtn.setDisabled(false);
-
 	    fd.append("content", view.content);
-
-	    const fileField = form.findField('file');
-	    const file = fileField.fileInputEl.dom.files[0];
-	    fileField.setDisabled(true);
-
-	    const filenameField = form.findField('filename');
-	    const filename = filenameField.getValue();
-	    filenameField.setDisabled(true);
-
-	    const algorithmField = form.findField('checksum-algorithm');
-	    algorithmField.setDisabled(true);
-	    if (algorithmField.getValue() !== '__default__') {
-		fd.append("checksum-algorithm", algorithmField.getValue());
-
-		const checksumField = form.findField('checksum');
-		fd.append("checksum", checksumField.getValue()?.trim());
-		checksumField.setDisabled(true);
+	    if (data.hash !== '__default__') {
+		fd.append("checksum-algorithm", data.hash);
+		fd.append("checksum", data.hashsum.trim());
 	    }
+	    fd.append("filename", data.file, data.filename);
 
-	    fd.append("filename", file, filename);
-
-	    pbar.setVisible(true);
-	    updateProgress(0);
-
-	    const xhr = new XMLHttpRequest();
-	    view.xhr = xhr;
 
+	    const xhr = data.xhr = new XMLHttpRequest();
 	    xhr.addEventListener("load", function(e) {
 		if (xhr.status === 200) {
-		    view.hide();
-
 		    const result = JSON.parse(xhr.response);
 		    const upid = result.data;
 		    Ext.create('Proxmox.window.TaskViewer', {
 			autoShow: true,
 			upid: upid,
-			taskDone: view.taskDone,
+			taskDone: function(success) {
+			    if (success) {
+				this.close();
+			    } else {
+				const widget = record.get('progressWidget');
+				widget.updateProgress(0, "ERROR");
+				widget.setStyle('background-color', 'red');
+			    }
+			},
 			listeners: {
 			    destroy: function() {
-				view.close();
+				me?.startUpload?.();
 			    },
 			},
 		    });
+		} else {
+		    const widget = record.get('progressWidget');
+		    widget.updateProgress(0, `ERROR: ${xhr.status}`);
+		    widget.setStyle('background-color', 'red');
+		    me.getViewModel().set('uploadInProgress', false);
+		}
+	    });
 
-		    return;
+	    const updateProgress = function(per, bytes) {
+		let text = (per * 100).toFixed(2) + '%';
+		if (bytes) {
+		    text += " (" + Proxmox.Utils.format_size(bytes) + ')';
 		}
-		const err = Ext.htmlEncode(xhr.statusText);
-		let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`;
-		if (xhr.responseText !== "") {
-		    const result = Ext.decode(xhr.responseText);
-		    result.message = msg;
-		    msg = Proxmox.Utils.extractRequestError(result, true);
-		}
-		Ext.Msg.alert(gettext('Error'), msg, btn => view.close());
-	    }, false);
+		let widget = record.get('progressWidget');
+		widget?.updateProgress(per, text);
+	    };
 
 	    xhr.addEventListener("error", function(e) {
 		const err = e.target.status.toString();
 		const msg = `Error '${err}' occurred while receiving the document.`;
-		Ext.Msg.alert(gettext('Error'), msg, btn => view.close());
+		Ext.Msg.alert(gettext('Error'), msg, _ => view.close());
 	    });
 
 	    xhr.upload.addEventListener("progress", function(evt) {
@@ -123,173 +142,181 @@ Ext.define('PVE.window.UploadToStorage', {
 		}
 	    }, false);
 
+	    me.getViewModel().set('uploadInProgress', true);
 	    xhr.open("POST", `/api2/json${view.url}`, true);
 	    xhr.send(fd);
 	},
 
-	validitychange: function(f, valid) {
-	    const submitBtn = this.lookup('submitBtn');
-	    submitBtn.setDisabled(!valid);
-	},
-
-	fileChange: function(input) {
-	    const vm = this.getViewModel();
-	    const name = input.value.replace(/^.*(\/|\\)/, '');
-	    const fileInput = input.fileInputEl.dom;
-	    vm.set('filename', name);
-	    vm.set('size', (fileInput.files[0] && Proxmox.Utils.format_size(fileInput.files[0].size)) || '-');
-	    vm.set('mimetype', (fileInput.files[0] && fileInput.files[0].type) || '-');
-	},
-
-	hashChange: function(field, value) {
-	    const checksum = this.lookup('downloadUrlChecksum');
-	    if (value === '__default__') {
-		checksum.setDisabled(true);
-		checksum.setValue("");
-	    } else {
-		checksum.setDisabled(false);
-	    }
+	removeFile: function(_view, _rowIndex, _colIndex, _item, _event, record) {
+	    let me = this;
+	    me.lookup('grid').store.remove(record);
+	    me.getViewModel().set('uploadInProgress', false);
 	},
     },
 
     items: [
 	{
-	    xtype: 'form',
-	    reference: 'formPanel',
-	    method: 'POST',
-	    waitMsgTarget: true,
-	    bodyPadding: 10,
-	    border: false,
-	    width: 400,
-	    fieldDefaults: {
-		labelWidth: 100,
-		anchor: '100%',
-            },
-	    items: [
-		{
-		    xtype: 'filefield',
-		    name: 'file',
-		    buttonText: gettext('Select File'),
-		    allowBlank: false,
-		    fieldLabel: gettext('File'),
-		    cbind: {
-			accept: '{extensions}',
-		    },
-		    listeners: {
-			change: 'fileChange',
+	    xtype: 'grid',
+	    reference: 'grid',
+	    height: 700,
+	    width: 1100,
+	    store: {
+		listeners: {
+		    remove: function(_store, records) {
+			records.forEach(record => {
+			    record.get('xhr')?.abort();
+			    record.progressWidget = null;
+			    record.hashWidget = null;
+			});
 		    },
 		},
+		model: 'pve-multiupload',
+	    },
+	    listeners: {
+		beforedestroy: function(grid) {
+		    grid.store.each(record => grid.store.remove(record));
+		},
+	    },
+	    columns: [
 		{
-		    xtype: 'textfield',
-		    name: 'filename',
-		    allowBlank: false,
-		    fieldLabel: gettext('File name'),
-		    bind: {
-			value: '{filename}',
-		    },
-		    cbind: {
-			regex: '{filenameRegex}',
-		    },
-		    regexText: gettext('Wrong file extension'),
+		    header: gettext('Source Name'),
+		    dataIndex: 'file',
+		    renderer: file => file.name,
+		    flex: 2,
 		},
 		{
-		    xtype: 'displayfield',
-		    name: 'size',
-		    fieldLabel: gettext('File size'),
-		    bind: {
-			value: '{size}',
+		    header: gettext('File Name'),
+		    dataIndex: 'filename',
+		    flex: 2,
+		    xtype: 'widgetcolumn',
+		    widget: {
+			xtype: 'textfield',
+			listeners: {
+			    change: function(widget, newValue, oldValue) {
+				const record = widget.getWidgetRecord();
+				record.set('filename', newValue);
+			    },
+			},
+			cbind: {
+			    regex: '{filenameRegex}',
+			},
+			regexText: gettext('Wrong file extension'),
 		    },
 		},
 		{
-		    xtype: 'displayfield',
-		    name: 'mimetype',
-		    fieldLabel: gettext('MIME type'),
-		    bind: {
-			value: '{mimetype}',
-		    },
+		    header: gettext('File size'),
+		    dataIndex: 'size',
 		},
 		{
-		    xtype: 'pveHashAlgorithmSelector',
-		    name: 'checksum-algorithm',
-		    fieldLabel: gettext('Hash algorithm'),
-		    allowBlank: true,
-		    hasNoneOption: true,
-		    value: '__default__',
-		    listeners: {
-			change: 'hashChange',
-		    },
+		    header: gettext('MIME type'),
+		    dataIndex: 'mimetype',
 		},
 		{
-		    xtype: 'textfield',
-		    name: 'checksum',
-		    fieldLabel: gettext('Checksum'),
-		    allowBlank: false,
-		    disabled: true,
-		    emptyText: gettext('none'),
-		    reference: 'downloadUrlChecksum',
+		    header: gettext('Hash'),
+		    dataIndex: 'hash',
+		    flex: 2,
+		    xtype: 'widgetcolumn',
+		    widget: {
+			xtype: 'pveHashAlgorithmSelector',
+			listeners: {
+			    change: function(widget, newValue, oldValue) {
+				const record = widget.getWidgetRecord();
+				record.set('hash', newValue);
+				let hashWidget = record.get('hashWidget');
+				if (newValue === '__default__') {
+					hashWidget?.setDisabled(true);
+					hashWidget?.setValue('');
+				} else {
+				    hashWidget?.setDisabled(false);
+				}
+			    },
+			},
+		    },
 		},
 		{
-		    xtype: 'progressbar',
-		    text: 'Ready',
-		    hidden: true,
-		    reference: 'progressBar',
+		    header: gettext('Hash Value'),
+		    dataIndex: 'hashsum',
+		    renderer: data => data || 'None',
+		    flex: 4,
+		    xtype: 'widgetcolumn',
+		    widget: {
+			xtype: 'textfield',
+			disabled: true,
+			listeners: {
+			    change: function(widget, newValue, oldValue) {
+				const record = widget.getWidgetRecord();
+				record.set('hashsum', newValue);
+			    },
+			},
+		    },
+		    onWidgetAttach: function(col, widget, record) {
+			record.set('hashWidget', widget);
+		    },
 		},
 		{
-		    xtype: 'hiddenfield',
-		    name: 'content',
-		    cbind: {
-			value: '{content}',
+		    header: gettext('Progress Bar'),
+		    xtype: 'widgetcolumn',
+		    widget: {
+			xtype: 'progressbar',
 		    },
+		    onWidgetAttach: function(col, widget, rec) {
+			rec.set('progressWidget', widget);
+			widget.updateProgress(0, "");
+		    },
+		    flex: 2,
+		},
+		{
+		    xtype: 'actioncolumn',
+		    items: [{
+			iconCls: 'fa critical fa-trash-o',
+			handler: 'removeFile',
+		    }],
+		    flex: 0.5,
 		},
 	    ],
-	   listeners: {
-		validitychange: 'validitychange',
-	   },
 	},
     ],
 
     buttons: [
+	{
+	    xtype: 'filefield',
+	    name: 'file',
+	    buttonText: gettext('Add File'),
+	    allowBlank: false,
+	    hideLabel: true,
+	    fieldStyle: 'display: none;',
+	    cbind: {
+		accept: '{extensions}',
+	    },
+	    listeners: {
+		change: 'addFile',
+		render: function(filefield) {
+		    filefield.fileInputEl.dom.multiple = true;
+		},
+	    },
+	},
 	{
 	    xtype: 'button',
 	    text: gettext('Abort'),
-	    reference: 'abortBtn',
-	    disabled: true,
 	    handler: function() {
 		const me = this;
 		me.up('pveStorageUpload').close();
 	    },
 	},
 	{
-	    text: gettext('Upload'),
-	    reference: 'submitBtn',
-	    disabled: true,
-	    handler: 'submit',
+	    text: gettext('Start upload'),
+	    handler: 'startUpload',
+	    bind: {
+		disabled: '{!hasFiles || uploadInProgress}',
+	    },
 	},
     ],
 
-    listeners: {
-	close: function() {
-	    const me = this;
-	    if (me.xhr) {
-		me.xhr.abort();
-		delete me.xhr;
-	    }
-	},
-    },
-
     initComponent: function() {
-        const me = this;
-
-	if (!me.nodename) {
-	    throw "no node name specified";
-	}
-	if (!me.storage) {
-	    throw "no storage ID specified";
-	}
-	if (!me.acceptedExtensions[me.content]) {
-	    throw "content type not supported";
-	}
-
-        me.callParent();
+	let me = this;
+	me.callParent();
+	me.lookup('grid').store.on('datachanged', function(store) {
+	    me.getViewModel().set('hasFiles', store.count() > 0);
+	});
     },
 });
-- 
2.30.2





More information about the pve-devel mailing list