[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