From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 743A578AD0 for ; Wed, 29 Jun 2022 14:23:50 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 65C50253BE for ; Wed, 29 Jun 2022 14:23:50 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 29 Jun 2022 14:23:49 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id D19EE43D66 for ; Wed, 29 Jun 2022 14:23:48 +0200 (CEST) From: Matthias Heiserer To: pve-devel@lists.proxmox.com Date: Wed, 29 Jun 2022 14:23:22 +0200 Message-Id: <20220629122322.816989-1-m.heiserer@proxmox.com> X-Mailer: git-send-email 2.30.2 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.189 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [RFC manager] fix #3248: GUI: storage: upload multiple files X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 29 Jun 2022 12:23:50 -0000 Allows queueing multiple files for upload to the storage, which wasn't possible with the old upload window. Signed-off-by: Matthias Heiserer --- 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