* [pve-devel] [PATCH v2 manager] fix #3248: GUI: storage: upload multiple files
@ 2022-07-20 12:26 Matthias Heiserer
2022-07-29 9:29 ` Markus Frank
2023-03-31 11:33 ` Matthias Heiserer
0 siblings, 2 replies; 3+ messages in thread
From: Matthias Heiserer @ 2022-07-20 12:26 UTC (permalink / raw)
To: pve-devel
Queue multiple files for upload to the storage.
The upload itself happens in a separate window.
When closing the window, files with an error (i.e. wrong hash)
are retained in the upload window.
Signed-off-by: Matthias Heiserer <m.heiserer@proxmox.com>
---
Depends on https://lists.proxmox.com/pipermail/pbs-devel/2022-July/005365.html
Without that, trashcan icons are invisible.
Changes from v1:
* separate into file selection window and upload window
* prohibit upload of files with invalid name or missing hash
* rename abort button to cancel
* prohibit upload of duplicate files (checked by name)
* move event handlers and initcomponet code to controller
* abort XHR when window is closed
* general code cleanup
* show tasklog only when pressing button
* display uploaded/total files and the current status at the top
www/manager6/.lint-incremental | 0
www/manager6/window/UploadToStorage.js | 633 +++++++++++++++++--------
2 files changed, 446 insertions(+), 187 deletions(-)
create mode 100644 www/manager6/.lint-incremental
diff --git a/www/manager6/.lint-incremental b/www/manager6/.lint-incremental
new file mode 100644
index 00000000..e69de29b
diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
index 0de6d89d..67780165 100644
--- a/www/manager6/window/UploadToStorage.js
+++ b/www/manager6/window/UploadToStorage.js
@@ -1,9 +1,25 @@
+Ext.define('pve-multiupload', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'file', 'filename', 'progressWidget', 'hashsum', 'hashValueWidget',
+ 'xhr', 'mimetype', 'size', 'fileNameWidget', 'hashWidget',
+ {
+ name: 'done', defaultValue: false,
+ },
+ {
+ name: 'hash', defaultValue: '__default__',
+ },
+ ],
+});
Ext.define('PVE.window.UploadToStorage', {
extend: 'Ext.window.Window',
alias: 'widget.pveStorageUpload',
mixins: ['Proxmox.Mixin.CBind'],
+ height: 400,
+ width: 800,
- resizable: false,
+ resizable: true,
+ scrollable: true,
modal: true,
title: gettext('Upload'),
@@ -27,93 +43,405 @@ Ext.define('PVE.window.UploadToStorage', {
viewModel: {
data: {
- size: '-',
- mimetype: '-',
- filename: '',
+ validFiles: 0,
+ numFiles: 0,
+ invalidHash: 0,
},
},
-
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) + ')';
+ init: function(view) {
+ const me = this;
+ me.lookup('grid').store.viewModel = me.getViewModel();
+ },
+
+ addFile: function(input) {
+ const me = this;
+ const grid = me.lookup('grid');
+ for (const file of input.fileInputEl.dom.files) {
+ if (grid.store.findBy(
+ record => record.get('file').name === file.name) >= 0
+ ) {
+ continue;
+ }
+ grid.store.add({
+ file: file,
+ filename: file.name,
+ size: Proxmox.Utils.format_size(file.size),
+ mimetype: file.type,
+ });
+ }
+ },
+
+ removeFileHandler: function(view, rowIndex, colIndex, item, event, record) {
+ const me = this;
+ me.removeFile(record);
+ },
+
+ removeFile: function(record) {
+ const me = this;
+ const widget = record.get('fileNameWidget');
+ // set filename to invalid value, so when adding a new file with valid name,
+ // the validityChange listener is called
+ widget.setValue("");
+ me.lookup('grid').store.remove(record);
+ },
+
+ openUploadWindow: function() {
+ const me = this;
+ const view = me.getView();
+ Ext.create('PVE.window.UploadProgress', {
+ store: Ext.create('Ext.data.ChainedStore', {
+ source: me.lookup('grid').store,
+ }),
+ nodename: view.nodename,
+ storage: view.storage,
+ content: view.content,
+ autoShow: true,
+ taskDone: view.taskDone,
+ numFiles: me.getViewModel().get('numFiles'),
+ listeners: {
+ close: function() {
+ const store = this.lookup('grid').store;
+ store.each(function(record) {
+ if (record.get('done')) {
+ me.removeFile(record);
+ }
+ });
+ },
+ },
+ });
+ },
+
+ fileNameChange: function(widget, newValue, oldValue) {
+ const record = widget.getWidgetRecord();
+ record.set('filename', newValue);
+ },
+
+ fileNameValidityChange: function(widget, isValid) {
+ const me = this;
+ const current = me.getViewModel().get('validFiles');
+ if (isValid) {
+ me.getViewModel().set('validFiles', current + 1);
+ } else {
+ me.getViewModel().set('validFiles', current - 1);
+ }
+ },
+
+ hashChange: function(widget, newValue, oldValue) {
+ const record = widget.getWidgetRecord();
+ // hashChange is called once before on WidgetAttach, so skip that
+ if (record) {
+ record.set('hash', newValue);
+ const hashValueWidget = record.get('hashValueWidget');
+ if (newValue === '__default__') {
+ hashValueWidget.setValue('');
+ hashValueWidget.setDisabled(true);
+ } else {
+ hashValueWidget.setDisabled(false);
+ hashValueWidget.validate();
+ }
+ }
+ },
+
+ hashValueChange: function(widget, newValue, oldValue) {
+ const record = widget.getWidgetRecord();
+ record.set('hashsum', newValue);
+ },
+
+ hashValueValidityChange: function(widget, isValid) {
+ const vm = this.getViewModel();
+ vm.set('invalidHash', vm.get('invalidHash') + (isValid ? -1 : 1));
+ },
+
+ breakCyclicReferences: function(grid) {
+ grid.store.each(record => grid.store.remove(record));
+ },
+
+ onHashWidgetAttach: function(col, widget, record) {
+ record.set('hashWidget', widget);
+ },
+
+ onHashValueWidgetAttach: function(col, widget, record) {
+ record.set('hashValueWidget', widget);
+ },
+
+ onFileNameWidgetAttach: function(col, widget, record) {
+ record.set('fileNameWidget', widget);
+ },
+
+ enableUploadOfMultipleFiles: function(filefield) {
+ filefield.fileInputEl.dom.multiple = true;
+ },
+ },
+
+ items: [
+ {
+ xtype: 'grid',
+ reference: 'grid',
+ store: {
+ listeners: {
+ remove: function(_store, records) {
+ records.forEach(record => {
+ record.get('xhr')?.abort();
+
+ // cleanup so change event won't trigger when adding next file
+ // as that would happen before the widget gets attached
+ record.get('hashWidget').setValue('__default__');
+
+ // remove cyclic references to the widgets
+ record.set('progressWidget', null);
+ record.set('hashWidget', null);
+ record.set('hashValueWidget', null);
+ record.set('fileNameWidget', null);
+
+ const me = this;
+ const numFiles = me.viewModel.get('numFiles');
+ me.viewModel.set('numFiles', numFiles - 1);
+ });
+ },
+ add: function(_store, records) {
+ const me = this;
+ const current = me.viewModel.get('numFiles');
+ me.viewModel.set('numFiles', current + 1);
+ },
+ },
+ model: 'pve-multiupload',
+ },
+ listeners: {
+ beforedestroy: 'breakCyclicReferences',
+ },
+ columns: [
+ {
+ header: gettext('Source Name'),
+ dataIndex: 'file',
+ renderer: file => file.name,
+ width: 200,
+ },
+ {
+ header: gettext('File Name'),
+ dataIndex: 'filename',
+ width: 300,
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'textfield',
+ listeners: {
+ change: 'fileNameChange',
+ validityChange: 'fileNameValidityChange',
+ },
+ cbind: {
+ regex: '{filenameRegex}',
+ },
+ regexText: gettext('Wrong file extension'),
+ allowBlank: false,
+ },
+ onWidgetAttach: 'onFileNameWidgetAttach',
+ },
+ {
+ header: gettext('File size'),
+ dataIndex: 'size',
+ },
+ {
+ header: gettext('MIME type'),
+ dataIndex: 'mimetype',
+ hidden: true,
+ },
+ {
+ xtype: 'actioncolumn',
+ items: [{
+ iconCls: 'fa critical fa-trash-o',
+ handler: 'removeFileHandler',
+ }],
+ width: 50,
+ },
+ {
+ header: gettext('Hash'),
+ dataIndex: 'hash',
+ width: 110,
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveHashAlgorithmSelector',
+ listeners: {
+ change: 'hashChange',
+ },
+ },
+ onWidgetAttach: 'onHashWidgetAttach',
+ },
+ {
+ header: gettext('Hash Value'),
+ dataIndex: 'hashsum',
+ renderer: data => data || 'None',
+ width: 300,
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'textfield',
+ disabled: true,
+ listeners: {
+ change: 'hashValueChange',
+ validityChange: 'hashValueValidityChange',
+ },
+ allowBlank: false,
+ },
+ onWidgetAttach: 'onHashValueWidgetAttach',
+ },
+ ],
+ },
+ ],
+
+ buttons: [
+ {
+ xtype: 'filefield',
+ name: 'file',
+ buttonText: gettext('Add File'),
+ allowBlank: false,
+ hideLabel: true,
+ fieldStyle: 'display: none;',
+ cbind: {
+ accept: '{extensions}',
+ },
+ listeners: {
+ change: 'addFile',
+ render: 'enableUploadOfMultipleFiles',
+ },
+ },
+ {
+ xtype: 'button',
+ text: gettext('Cancel'),
+ handler: function() {
+ const me = this;
+ me.up('pveStorageUpload').close();
+ },
+ },
+ {
+ text: gettext('Start upload'),
+ handler: 'openUploadWindow',
+ bind: {
+ disabled: '{numFiles == 0 || !(validFiles === numFiles) || invalidHash}',
+ },
+ },
+ ],
+});
+
+Ext.define('PVE.window.UploadProgress', {
+ extend: 'Ext.window.Window',
+ alias: 'widget.pveStorageUploadProgress',
+ mixins: ['Proxmox.Mixin.CBind'],
+ height: 400,
+ width: 800,
+ resizable: true,
+ scrollable: true,
+ modal: true,
+
+ cbindData: function(initialConfig) {
+ const me = this;
+ const ext = me.acceptedExtensions[me.content] || [];
+
+ me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
+
+ return {
+ extensions: ext.join(', '),
+ filenameRegex: RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
+ store: initialConfig.store,
+ };
+ },
+
+
+ title: gettext('Upload Progress'),
+
+ acceptedExtensions: {
+ iso: ['.img', '.iso'],
+ vztmpl: ['.tar.gz', '.tar.xz'],
+ },
+
+ viewModel: {
+ data: {
+ numUploaded: 0,
+ numFiles: 0,
+ currentTask: '',
+ },
+
+ formulas: {
+ loadingLabel: function(get) {
+ if (get('currentTask') === 'Copy files') {
+ return 'x-grid-row-loading';
}
- pbar.updateProgress(per, text);
- };
+ return '';
+ },
+ },
+ },
+ controller: {
+ init: function(view) {
+ const me = this;
+ me.getViewModel().data.numFiles = view.numFiles;
+ me.startUpload();
+ },
+
+ currentUploadIndex: 0,
+ startUpload: function() {
+ const me = this;
+ const view = me.getView();
+ const grid = me.lookup('grid');
+ const vm = me.getViewModel();
+
+ const record = grid.store.getAt(me.currentUploadIndex++);
+ if (!record) {
+ vm.set('currentTask', 'Done');
+ 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) {
+ vm.set('currentTask', 'Copy files');
if (xhr.status === 200) {
- view.hide();
-
const result = JSON.parse(xhr.response);
const upid = result.data;
- Ext.create('Proxmox.window.TaskViewer', {
- autoShow: true,
+ const taskviewer = Ext.create('Proxmox.window.TaskViewer', {
upid: upid,
- taskDone: view.taskDone,
- listeners: {
- destroy: function() {
- view.close();
- },
+ taskDone: function(success) {
+ vm.set('numUploaded', vm.get('numUploaded') + 1);
+ if (success) {
+ record.set('done', true);
+ } else {
+ const widget = record.get('progressWidget');
+ widget.updateProgress(0, "ERROR");
+ widget.setStyle('background-color', 'red');
+ }
+ view.taskDone();
+ me.startUpload();
},
+ closeAction: 'hide',
});
+ record.set('taskviewer', taskviewer);
+ record.get('taskViewerButton').enable();
+ } else {
+ const widget = record.get('progressWidget');
+ widget.updateProgress(0, `ERROR: ${xhr.status}`);
+ widget.setStyle('background-color', 'red');
+ me.startUpload();
+ }
+ });
- 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);
+ record.get('progressWidget').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) {
@@ -125,171 +453,102 @@ Ext.define('PVE.window.UploadToStorage', {
xhr.open("POST", `/api2/json${view.url}`, true);
xhr.send(fd);
+ vm.set('currentTask', 'Upload files');
},
- validitychange: function(f, valid) {
- const submitBtn = this.lookup('submitBtn');
- submitBtn.setDisabled(!valid);
+ onProgressWidgetAttach: function(col, widget, rec) {
+ rec.set('progressWidget', widget);
+ widget.updateProgress(0, "");
},
- 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) || '-');
+ onWindowClose: function(panel) {
+ const store = panel.lookup('grid').store;
+ store.each(record => record.get('xhr')?.abort());
},
- hashChange: function(field, value) {
- const checksum = this.lookup('downloadUrlChecksum');
- if (value === '__default__') {
- checksum.setDisabled(true);
- checksum.setValue("");
- } else {
- checksum.setDisabled(false);
- }
+ showTaskViewer: function(button) {
+ button.record.get('taskviewer').show();
+ },
+
+ onTaskButtonAttach: function(col, widget, rec) {
+ widget.record = rec;
+ rec.set('taskViewerButton', widget);
},
},
items: [
{
- xtype: 'form',
- reference: 'formPanel',
- method: 'POST',
- waitMsgTarget: true,
- bodyPadding: 10,
- border: false,
- width: 400,
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%',
- },
- items: [
+ xtype: 'grid',
+ reference: 'grid',
+
+ cbind: {
+ store: '{store}',
+ },
+ columns: [
{
- xtype: 'filefield',
- name: 'file',
- buttonText: gettext('Select File'),
- allowBlank: false,
- fieldLabel: gettext('File'),
- cbind: {
- accept: '{extensions}',
- },
- listeners: {
- change: 'fileChange',
- },
+ header: gettext('File Name'),
+ dataIndex: 'filename',
+ flex: 4,
},
{
- xtype: 'textfield',
- name: 'filename',
- allowBlank: false,
- fieldLabel: gettext('File name'),
- bind: {
- value: '{filename}',
- },
- cbind: {
- regex: '{filenameRegex}',
+ header: gettext('Progress Bar'),
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'progressbar',
},
- regexText: gettext('Wrong file extension'),
+ onWidgetAttach: 'onProgressWidgetAttach',
+ flex: 2,
},
{
- xtype: 'displayfield',
- name: 'size',
- fieldLabel: gettext('File size'),
- bind: {
- value: '{size}',
+ header: gettext('Task Viewer'),
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'button',
+ handler: 'showTaskViewer',
+ disabled: true,
+ text: 'Show',
},
+ onWidgetAttach: 'onTaskButtonAttach',
},
+ ],
+ tbar: [
{
xtype: 'displayfield',
- name: 'mimetype',
- fieldLabel: gettext('MIME type'),
bind: {
- value: '{mimetype}',
+ value: 'Files uploaded: {numUploaded} / {numFiles}',
},
},
+ '->',
{
- xtype: 'pveHashAlgorithmSelector',
- name: 'checksum-algorithm',
- fieldLabel: gettext('Hash algorithm'),
- allowBlank: true,
- hasNoneOption: true,
- value: '__default__',
- listeners: {
- change: 'hashChange',
+ xtype: 'displayfield',
+ bind: {
+ value: '{currentTask}',
},
},
{
- xtype: 'textfield',
- name: 'checksum',
- fieldLabel: gettext('Checksum'),
- allowBlank: false,
- disabled: true,
- emptyText: gettext('none'),
- reference: 'downloadUrlChecksum',
- },
- {
- xtype: 'progressbar',
- text: 'Ready',
- hidden: true,
- reference: 'progressBar',
- },
- {
- xtype: 'hiddenfield',
- name: 'content',
- cbind: {
- value: '{content}',
+ xtype: 'displayfield',
+ userCls: 'x-grid-row-loading',
+ width: 30,
+ bind: {
+ hidden: '{currentTask === "Done"}',
},
},
],
- listeners: {
- validitychange: 'validitychange',
- },
},
],
buttons: [
{
xtype: 'button',
- text: gettext('Abort'),
- reference: 'abortBtn',
- disabled: true,
+ text: gettext('Exit'),
handler: function() {
const me = this;
- me.up('pveStorageUpload').close();
+ me.up('pveStorageUploadProgress').close();
},
},
- {
- text: gettext('Upload'),
- reference: 'submitBtn',
- disabled: true,
- handler: 'submit',
- },
],
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();
+ close: 'onWindowClose',
},
});
--
2.30.2
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [pve-devel] [PATCH v2 manager] fix #3248: GUI: storage: upload multiple files
2022-07-20 12:26 [pve-devel] [PATCH v2 manager] fix #3248: GUI: storage: upload multiple files Matthias Heiserer
@ 2022-07-29 9:29 ` Markus Frank
2023-03-31 11:33 ` Matthias Heiserer
1 sibling, 0 replies; 3+ messages in thread
From: Markus Frank @ 2022-07-29 9:29 UTC (permalink / raw)
To: Proxmox VE development discussion
I tested this patch in a pve vm with multiple different iso files.
Upload, Cancel, Remove and Exit work as intended.
Tested-by: Markus Frank <m.frank@proxmox.com>
On 7/20/22 14:26, Matthias Heiserer wrote:
> Queue multiple files for upload to the storage.
> The upload itself happens in a separate window.
> When closing the window, files with an error (i.e. wrong hash)
> are retained in the upload window.
>
> Signed-off-by: Matthias Heiserer <m.heiserer@proxmox.com>
> ---
>
> Depends on https://lists.proxmox.com/pipermail/pbs-devel/2022-July/005365.html
> Without that, trashcan icons are invisible.
>
> Changes from v1:
> * separate into file selection window and upload window
> * prohibit upload of files with invalid name or missing hash
> * rename abort button to cancel
> * prohibit upload of duplicate files (checked by name)
> * move event handlers and initcomponet code to controller
> * abort XHR when window is closed
> * general code cleanup
> * show tasklog only when pressing button
> * display uploaded/total files and the current status at the top
>
> www/manager6/.lint-incremental | 0
> www/manager6/window/UploadToStorage.js | 633 +++++++++++++++++--------
> 2 files changed, 446 insertions(+), 187 deletions(-)
> create mode 100644 www/manager6/.lint-incremental
>
> diff --git a/www/manager6/.lint-incremental b/www/manager6/.lint-incremental
> new file mode 100644
> index 00000000..e69de29b
> diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
> index 0de6d89d..67780165 100644
> --- a/www/manager6/window/UploadToStorage.js
> +++ b/www/manager6/window/UploadToStorage.js
> @@ -1,9 +1,25 @@
> +Ext.define('pve-multiupload', {
> + extend: 'Ext.data.Model',
> + fields: [
> + 'file', 'filename', 'progressWidget', 'hashsum', 'hashValueWidget',
> + 'xhr', 'mimetype', 'size', 'fileNameWidget', 'hashWidget',
> + {
> + name: 'done', defaultValue: false,
> + },
> + {
> + name: 'hash', defaultValue: '__default__',
> + },
> + ],
> +});
> Ext.define('PVE.window.UploadToStorage', {
> extend: 'Ext.window.Window',
> alias: 'widget.pveStorageUpload',
> mixins: ['Proxmox.Mixin.CBind'],
> + height: 400,
> + width: 800,
>
> - resizable: false,
> + resizable: true,
> + scrollable: true,
> modal: true,
>
> title: gettext('Upload'),
> @@ -27,93 +43,405 @@ Ext.define('PVE.window.UploadToStorage', {
>
> viewModel: {
> data: {
> - size: '-',
> - mimetype: '-',
> - filename: '',
> + validFiles: 0,
> + numFiles: 0,
> + invalidHash: 0,
> },
> },
> -
> 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) + ')';
> + init: function(view) {
> + const me = this;
> + me.lookup('grid').store.viewModel = me.getViewModel();
> + },
> +
> + addFile: function(input) {
> + const me = this;
> + const grid = me.lookup('grid');
> + for (const file of input.fileInputEl.dom.files) {
> + if (grid.store.findBy(
> + record => record.get('file').name === file.name) >= 0
> + ) {
> + continue;
> + }
> + grid.store.add({
> + file: file,
> + filename: file.name,
> + size: Proxmox.Utils.format_size(file.size),
> + mimetype: file.type,
> + });
> + }
> + },
> +
> + removeFileHandler: function(view, rowIndex, colIndex, item, event, record) {
> + const me = this;
> + me.removeFile(record);
> + },
> +
> + removeFile: function(record) {
> + const me = this;
> + const widget = record.get('fileNameWidget');
> + // set filename to invalid value, so when adding a new file with valid name,
> + // the validityChange listener is called
> + widget.setValue("");
> + me.lookup('grid').store.remove(record);
> + },
> +
> + openUploadWindow: function() {
> + const me = this;
> + const view = me.getView();
> + Ext.create('PVE.window.UploadProgress', {
> + store: Ext.create('Ext.data.ChainedStore', {
> + source: me.lookup('grid').store,
> + }),
> + nodename: view.nodename,
> + storage: view.storage,
> + content: view.content,
> + autoShow: true,
> + taskDone: view.taskDone,
> + numFiles: me.getViewModel().get('numFiles'),
> + listeners: {
> + close: function() {
> + const store = this.lookup('grid').store;
> + store.each(function(record) {
> + if (record.get('done')) {
> + me.removeFile(record);
> + }
> + });
> + },
> + },
> + });
> + },
> +
> + fileNameChange: function(widget, newValue, oldValue) {
> + const record = widget.getWidgetRecord();
> + record.set('filename', newValue);
> + },
> +
> + fileNameValidityChange: function(widget, isValid) {
> + const me = this;
> + const current = me.getViewModel().get('validFiles');
> + if (isValid) {
> + me.getViewModel().set('validFiles', current + 1);
> + } else {
> + me.getViewModel().set('validFiles', current - 1);
> + }
> + },
> +
> + hashChange: function(widget, newValue, oldValue) {
> + const record = widget.getWidgetRecord();
> + // hashChange is called once before on WidgetAttach, so skip that
> + if (record) {
> + record.set('hash', newValue);
> + const hashValueWidget = record.get('hashValueWidget');
> + if (newValue === '__default__') {
> + hashValueWidget.setValue('');
> + hashValueWidget.setDisabled(true);
> + } else {
> + hashValueWidget.setDisabled(false);
> + hashValueWidget.validate();
> + }
> + }
> + },
> +
> + hashValueChange: function(widget, newValue, oldValue) {
> + const record = widget.getWidgetRecord();
> + record.set('hashsum', newValue);
> + },
> +
> + hashValueValidityChange: function(widget, isValid) {
> + const vm = this.getViewModel();
> + vm.set('invalidHash', vm.get('invalidHash') + (isValid ? -1 : 1));
> + },
> +
> + breakCyclicReferences: function(grid) {
> + grid.store.each(record => grid.store.remove(record));
> + },
> +
> + onHashWidgetAttach: function(col, widget, record) {
> + record.set('hashWidget', widget);
> + },
> +
> + onHashValueWidgetAttach: function(col, widget, record) {
> + record.set('hashValueWidget', widget);
> + },
> +
> + onFileNameWidgetAttach: function(col, widget, record) {
> + record.set('fileNameWidget', widget);
> + },
> +
> + enableUploadOfMultipleFiles: function(filefield) {
> + filefield.fileInputEl.dom.multiple = true;
> + },
> + },
> +
> + items: [
> + {
> + xtype: 'grid',
> + reference: 'grid',
> + store: {
> + listeners: {
> + remove: function(_store, records) {
> + records.forEach(record => {
> + record.get('xhr')?.abort();
> +
> + // cleanup so change event won't trigger when adding next file
> + // as that would happen before the widget gets attached
> + record.get('hashWidget').setValue('__default__');
> +
> + // remove cyclic references to the widgets
> + record.set('progressWidget', null);
> + record.set('hashWidget', null);
> + record.set('hashValueWidget', null);
> + record.set('fileNameWidget', null);
> +
> + const me = this;
> + const numFiles = me.viewModel.get('numFiles');
> + me.viewModel.set('numFiles', numFiles - 1);
> + });
> + },
> + add: function(_store, records) {
> + const me = this;
> + const current = me.viewModel.get('numFiles');
> + me.viewModel.set('numFiles', current + 1);
> + },
> + },
> + model: 'pve-multiupload',
> + },
> + listeners: {
> + beforedestroy: 'breakCyclicReferences',
> + },
> + columns: [
> + {
> + header: gettext('Source Name'),
> + dataIndex: 'file',
> + renderer: file => file.name,
> + width: 200,
> + },
> + {
> + header: gettext('File Name'),
> + dataIndex: 'filename',
> + width: 300,
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'textfield',
> + listeners: {
> + change: 'fileNameChange',
> + validityChange: 'fileNameValidityChange',
> + },
> + cbind: {
> + regex: '{filenameRegex}',
> + },
> + regexText: gettext('Wrong file extension'),
> + allowBlank: false,
> + },
> + onWidgetAttach: 'onFileNameWidgetAttach',
> + },
> + {
> + header: gettext('File size'),
> + dataIndex: 'size',
> + },
> + {
> + header: gettext('MIME type'),
> + dataIndex: 'mimetype',
> + hidden: true,
> + },
> + {
> + xtype: 'actioncolumn',
> + items: [{
> + iconCls: 'fa critical fa-trash-o',
> + handler: 'removeFileHandler',
> + }],
> + width: 50,
> + },
> + {
> + header: gettext('Hash'),
> + dataIndex: 'hash',
> + width: 110,
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'pveHashAlgorithmSelector',
> + listeners: {
> + change: 'hashChange',
> + },
> + },
> + onWidgetAttach: 'onHashWidgetAttach',
> + },
> + {
> + header: gettext('Hash Value'),
> + dataIndex: 'hashsum',
> + renderer: data => data || 'None',
> + width: 300,
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'textfield',
> + disabled: true,
> + listeners: {
> + change: 'hashValueChange',
> + validityChange: 'hashValueValidityChange',
> + },
> + allowBlank: false,
> + },
> + onWidgetAttach: 'onHashValueWidgetAttach',
> + },
> + ],
> + },
> + ],
> +
> + buttons: [
> + {
> + xtype: 'filefield',
> + name: 'file',
> + buttonText: gettext('Add File'),
> + allowBlank: false,
> + hideLabel: true,
> + fieldStyle: 'display: none;',
> + cbind: {
> + accept: '{extensions}',
> + },
> + listeners: {
> + change: 'addFile',
> + render: 'enableUploadOfMultipleFiles',
> + },
> + },
> + {
> + xtype: 'button',
> + text: gettext('Cancel'),
> + handler: function() {
> + const me = this;
> + me.up('pveStorageUpload').close();
> + },
> + },
> + {
> + text: gettext('Start upload'),
> + handler: 'openUploadWindow',
> + bind: {
> + disabled: '{numFiles == 0 || !(validFiles === numFiles) || invalidHash}',
> + },
> + },
> + ],
> +});
> +
> +Ext.define('PVE.window.UploadProgress', {
> + extend: 'Ext.window.Window',
> + alias: 'widget.pveStorageUploadProgress',
> + mixins: ['Proxmox.Mixin.CBind'],
> + height: 400,
> + width: 800,
> + resizable: true,
> + scrollable: true,
> + modal: true,
> +
> + cbindData: function(initialConfig) {
> + const me = this;
> + const ext = me.acceptedExtensions[me.content] || [];
> +
> + me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
> +
> + return {
> + extensions: ext.join(', '),
> + filenameRegex: RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
> + store: initialConfig.store,
> + };
> + },
> +
> +
> + title: gettext('Upload Progress'),
> +
> + acceptedExtensions: {
> + iso: ['.img', '.iso'],
> + vztmpl: ['.tar.gz', '.tar.xz'],
> + },
> +
> + viewModel: {
> + data: {
> + numUploaded: 0,
> + numFiles: 0,
> + currentTask: '',
> + },
> +
> + formulas: {
> + loadingLabel: function(get) {
> + if (get('currentTask') === 'Copy files') {
> + return 'x-grid-row-loading';
> }
> - pbar.updateProgress(per, text);
> - };
> + return '';
> + },
> + },
> + },
> + controller: {
> + init: function(view) {
> + const me = this;
> + me.getViewModel().data.numFiles = view.numFiles;
> + me.startUpload();
> + },
> +
> + currentUploadIndex: 0,
> + startUpload: function() {
> + const me = this;
> + const view = me.getView();
> + const grid = me.lookup('grid');
> + const vm = me.getViewModel();
> +
> + const record = grid.store.getAt(me.currentUploadIndex++);
> + if (!record) {
> + vm.set('currentTask', 'Done');
> + 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) {
> + vm.set('currentTask', 'Copy files');
> if (xhr.status === 200) {
> - view.hide();
> -
> const result = JSON.parse(xhr.response);
> const upid = result.data;
> - Ext.create('Proxmox.window.TaskViewer', {
> - autoShow: true,
> + const taskviewer = Ext.create('Proxmox.window.TaskViewer', {
> upid: upid,
> - taskDone: view.taskDone,
> - listeners: {
> - destroy: function() {
> - view.close();
> - },
> + taskDone: function(success) {
> + vm.set('numUploaded', vm.get('numUploaded') + 1);
> + if (success) {
> + record.set('done', true);
> + } else {
> + const widget = record.get('progressWidget');
> + widget.updateProgress(0, "ERROR");
> + widget.setStyle('background-color', 'red');
> + }
> + view.taskDone();
> + me.startUpload();
> },
> + closeAction: 'hide',
> });
> + record.set('taskviewer', taskviewer);
> + record.get('taskViewerButton').enable();
> + } else {
> + const widget = record.get('progressWidget');
> + widget.updateProgress(0, `ERROR: ${xhr.status}`);
> + widget.setStyle('background-color', 'red');
> + me.startUpload();
> + }
> + });
>
> - 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);
> + record.get('progressWidget').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) {
> @@ -125,171 +453,102 @@ Ext.define('PVE.window.UploadToStorage', {
>
> xhr.open("POST", `/api2/json${view.url}`, true);
> xhr.send(fd);
> + vm.set('currentTask', 'Upload files');
> },
>
> - validitychange: function(f, valid) {
> - const submitBtn = this.lookup('submitBtn');
> - submitBtn.setDisabled(!valid);
> + onProgressWidgetAttach: function(col, widget, rec) {
> + rec.set('progressWidget', widget);
> + widget.updateProgress(0, "");
> },
>
> - 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) || '-');
> + onWindowClose: function(panel) {
> + const store = panel.lookup('grid').store;
> + store.each(record => record.get('xhr')?.abort());
> },
>
> - hashChange: function(field, value) {
> - const checksum = this.lookup('downloadUrlChecksum');
> - if (value === '__default__') {
> - checksum.setDisabled(true);
> - checksum.setValue("");
> - } else {
> - checksum.setDisabled(false);
> - }
> + showTaskViewer: function(button) {
> + button.record.get('taskviewer').show();
> + },
> +
> + onTaskButtonAttach: function(col, widget, rec) {
> + widget.record = rec;
> + rec.set('taskViewerButton', widget);
> },
> },
>
> items: [
> {
> - xtype: 'form',
> - reference: 'formPanel',
> - method: 'POST',
> - waitMsgTarget: true,
> - bodyPadding: 10,
> - border: false,
> - width: 400,
> - fieldDefaults: {
> - labelWidth: 100,
> - anchor: '100%',
> - },
> - items: [
> + xtype: 'grid',
> + reference: 'grid',
> +
> + cbind: {
> + store: '{store}',
> + },
> + columns: [
> {
> - xtype: 'filefield',
> - name: 'file',
> - buttonText: gettext('Select File'),
> - allowBlank: false,
> - fieldLabel: gettext('File'),
> - cbind: {
> - accept: '{extensions}',
> - },
> - listeners: {
> - change: 'fileChange',
> - },
> + header: gettext('File Name'),
> + dataIndex: 'filename',
> + flex: 4,
> },
> {
> - xtype: 'textfield',
> - name: 'filename',
> - allowBlank: false,
> - fieldLabel: gettext('File name'),
> - bind: {
> - value: '{filename}',
> - },
> - cbind: {
> - regex: '{filenameRegex}',
> + header: gettext('Progress Bar'),
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'progressbar',
> },
> - regexText: gettext('Wrong file extension'),
> + onWidgetAttach: 'onProgressWidgetAttach',
> + flex: 2,
> },
> {
> - xtype: 'displayfield',
> - name: 'size',
> - fieldLabel: gettext('File size'),
> - bind: {
> - value: '{size}',
> + header: gettext('Task Viewer'),
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'button',
> + handler: 'showTaskViewer',
> + disabled: true,
> + text: 'Show',
> },
> + onWidgetAttach: 'onTaskButtonAttach',
> },
> + ],
> + tbar: [
> {
> xtype: 'displayfield',
> - name: 'mimetype',
> - fieldLabel: gettext('MIME type'),
> bind: {
> - value: '{mimetype}',
> + value: 'Files uploaded: {numUploaded} / {numFiles}',
> },
> },
> + '->',
> {
> - xtype: 'pveHashAlgorithmSelector',
> - name: 'checksum-algorithm',
> - fieldLabel: gettext('Hash algorithm'),
> - allowBlank: true,
> - hasNoneOption: true,
> - value: '__default__',
> - listeners: {
> - change: 'hashChange',
> + xtype: 'displayfield',
> + bind: {
> + value: '{currentTask}',
> },
> },
> {
> - xtype: 'textfield',
> - name: 'checksum',
> - fieldLabel: gettext('Checksum'),
> - allowBlank: false,
> - disabled: true,
> - emptyText: gettext('none'),
> - reference: 'downloadUrlChecksum',
> - },
> - {
> - xtype: 'progressbar',
> - text: 'Ready',
> - hidden: true,
> - reference: 'progressBar',
> - },
> - {
> - xtype: 'hiddenfield',
> - name: 'content',
> - cbind: {
> - value: '{content}',
> + xtype: 'displayfield',
> + userCls: 'x-grid-row-loading',
> + width: 30,
> + bind: {
> + hidden: '{currentTask === "Done"}',
> },
> },
> ],
> - listeners: {
> - validitychange: 'validitychange',
> - },
> },
> ],
>
> buttons: [
> {
> xtype: 'button',
> - text: gettext('Abort'),
> - reference: 'abortBtn',
> - disabled: true,
> + text: gettext('Exit'),
> handler: function() {
> const me = this;
> - me.up('pveStorageUpload').close();
> + me.up('pveStorageUploadProgress').close();
> },
> },
> - {
> - text: gettext('Upload'),
> - reference: 'submitBtn',
> - disabled: true,
> - handler: 'submit',
> - },
> ],
>
> 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();
> + close: 'onWindowClose',
> },
> });
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [pve-devel] [PATCH v2 manager] fix #3248: GUI: storage: upload multiple files
2022-07-20 12:26 [pve-devel] [PATCH v2 manager] fix #3248: GUI: storage: upload multiple files Matthias Heiserer
2022-07-29 9:29 ` Markus Frank
@ 2023-03-31 11:33 ` Matthias Heiserer
1 sibling, 0 replies; 3+ messages in thread
From: Matthias Heiserer @ 2023-03-31 11:33 UTC (permalink / raw)
To: pve-devel
ping - still applies
On 20.07.2022 14:26, Matthias Heiserer wrote:
> Queue multiple files for upload to the storage.
> The upload itself happens in a separate window.
> When closing the window, files with an error (i.e. wrong hash)
> are retained in the upload window.
>
> Signed-off-by: Matthias Heiserer <m.heiserer@proxmox.com>
> ---
>
> Depends on https://lists.proxmox.com/pipermail/pbs-devel/2022-July/005365.html
> Without that, trashcan icons are invisible.
>
> Changes from v1:
> * separate into file selection window and upload window
> * prohibit upload of files with invalid name or missing hash
> * rename abort button to cancel
> * prohibit upload of duplicate files (checked by name)
> * move event handlers and initcomponet code to controller
> * abort XHR when window is closed
> * general code cleanup
> * show tasklog only when pressing button
> * display uploaded/total files and the current status at the top
>
> www/manager6/.lint-incremental | 0
> www/manager6/window/UploadToStorage.js | 633 +++++++++++++++++--------
> 2 files changed, 446 insertions(+), 187 deletions(-)
> create mode 100644 www/manager6/.lint-incremental
>
> diff --git a/www/manager6/.lint-incremental b/www/manager6/.lint-incremental
> new file mode 100644
> index 00000000..e69de29b
> diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
> index 0de6d89d..67780165 100644
> --- a/www/manager6/window/UploadToStorage.js
> +++ b/www/manager6/window/UploadToStorage.js
> @@ -1,9 +1,25 @@
> +Ext.define('pve-multiupload', {
> + extend: 'Ext.data.Model',
> + fields: [
> + 'file', 'filename', 'progressWidget', 'hashsum', 'hashValueWidget',
> + 'xhr', 'mimetype', 'size', 'fileNameWidget', 'hashWidget',
> + {
> + name: 'done', defaultValue: false,
> + },
> + {
> + name: 'hash', defaultValue: '__default__',
> + },
> + ],
> +});
> Ext.define('PVE.window.UploadToStorage', {
> extend: 'Ext.window.Window',
> alias: 'widget.pveStorageUpload',
> mixins: ['Proxmox.Mixin.CBind'],
> + height: 400,
> + width: 800,
>
> - resizable: false,
> + resizable: true,
> + scrollable: true,
> modal: true,
>
> title: gettext('Upload'),
> @@ -27,93 +43,405 @@ Ext.define('PVE.window.UploadToStorage', {
>
> viewModel: {
> data: {
> - size: '-',
> - mimetype: '-',
> - filename: '',
> + validFiles: 0,
> + numFiles: 0,
> + invalidHash: 0,
> },
> },
> -
> 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) + ')';
> + init: function(view) {
> + const me = this;
> + me.lookup('grid').store.viewModel = me.getViewModel();
> + },
> +
> + addFile: function(input) {
> + const me = this;
> + const grid = me.lookup('grid');
> + for (const file of input.fileInputEl.dom.files) {
> + if (grid.store.findBy(
> + record => record.get('file').name === file.name) >= 0
> + ) {
> + continue;
> + }
> + grid.store.add({
> + file: file,
> + filename: file.name,
> + size: Proxmox.Utils.format_size(file.size),
> + mimetype: file.type,
> + });
> + }
> + },
> +
> + removeFileHandler: function(view, rowIndex, colIndex, item, event, record) {
> + const me = this;
> + me.removeFile(record);
> + },
> +
> + removeFile: function(record) {
> + const me = this;
> + const widget = record.get('fileNameWidget');
> + // set filename to invalid value, so when adding a new file with valid name,
> + // the validityChange listener is called
> + widget.setValue("");
> + me.lookup('grid').store.remove(record);
> + },
> +
> + openUploadWindow: function() {
> + const me = this;
> + const view = me.getView();
> + Ext.create('PVE.window.UploadProgress', {
> + store: Ext.create('Ext.data.ChainedStore', {
> + source: me.lookup('grid').store,
> + }),
> + nodename: view.nodename,
> + storage: view.storage,
> + content: view.content,
> + autoShow: true,
> + taskDone: view.taskDone,
> + numFiles: me.getViewModel().get('numFiles'),
> + listeners: {
> + close: function() {
> + const store = this.lookup('grid').store;
> + store.each(function(record) {
> + if (record.get('done')) {
> + me.removeFile(record);
> + }
> + });
> + },
> + },
> + });
> + },
> +
> + fileNameChange: function(widget, newValue, oldValue) {
> + const record = widget.getWidgetRecord();
> + record.set('filename', newValue);
> + },
> +
> + fileNameValidityChange: function(widget, isValid) {
> + const me = this;
> + const current = me.getViewModel().get('validFiles');
> + if (isValid) {
> + me.getViewModel().set('validFiles', current + 1);
> + } else {
> + me.getViewModel().set('validFiles', current - 1);
> + }
> + },
> +
> + hashChange: function(widget, newValue, oldValue) {
> + const record = widget.getWidgetRecord();
> + // hashChange is called once before on WidgetAttach, so skip that
> + if (record) {
> + record.set('hash', newValue);
> + const hashValueWidget = record.get('hashValueWidget');
> + if (newValue === '__default__') {
> + hashValueWidget.setValue('');
> + hashValueWidget.setDisabled(true);
> + } else {
> + hashValueWidget.setDisabled(false);
> + hashValueWidget.validate();
> + }
> + }
> + },
> +
> + hashValueChange: function(widget, newValue, oldValue) {
> + const record = widget.getWidgetRecord();
> + record.set('hashsum', newValue);
> + },
> +
> + hashValueValidityChange: function(widget, isValid) {
> + const vm = this.getViewModel();
> + vm.set('invalidHash', vm.get('invalidHash') + (isValid ? -1 : 1));
> + },
> +
> + breakCyclicReferences: function(grid) {
> + grid.store.each(record => grid.store.remove(record));
> + },
> +
> + onHashWidgetAttach: function(col, widget, record) {
> + record.set('hashWidget', widget);
> + },
> +
> + onHashValueWidgetAttach: function(col, widget, record) {
> + record.set('hashValueWidget', widget);
> + },
> +
> + onFileNameWidgetAttach: function(col, widget, record) {
> + record.set('fileNameWidget', widget);
> + },
> +
> + enableUploadOfMultipleFiles: function(filefield) {
> + filefield.fileInputEl.dom.multiple = true;
> + },
> + },
> +
> + items: [
> + {
> + xtype: 'grid',
> + reference: 'grid',
> + store: {
> + listeners: {
> + remove: function(_store, records) {
> + records.forEach(record => {
> + record.get('xhr')?.abort();
> +
> + // cleanup so change event won't trigger when adding next file
> + // as that would happen before the widget gets attached
> + record.get('hashWidget').setValue('__default__');
> +
> + // remove cyclic references to the widgets
> + record.set('progressWidget', null);
> + record.set('hashWidget', null);
> + record.set('hashValueWidget', null);
> + record.set('fileNameWidget', null);
> +
> + const me = this;
> + const numFiles = me.viewModel.get('numFiles');
> + me.viewModel.set('numFiles', numFiles - 1);
> + });
> + },
> + add: function(_store, records) {
> + const me = this;
> + const current = me.viewModel.get('numFiles');
> + me.viewModel.set('numFiles', current + 1);
> + },
> + },
> + model: 'pve-multiupload',
> + },
> + listeners: {
> + beforedestroy: 'breakCyclicReferences',
> + },
> + columns: [
> + {
> + header: gettext('Source Name'),
> + dataIndex: 'file',
> + renderer: file => file.name,
> + width: 200,
> + },
> + {
> + header: gettext('File Name'),
> + dataIndex: 'filename',
> + width: 300,
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'textfield',
> + listeners: {
> + change: 'fileNameChange',
> + validityChange: 'fileNameValidityChange',
> + },
> + cbind: {
> + regex: '{filenameRegex}',
> + },
> + regexText: gettext('Wrong file extension'),
> + allowBlank: false,
> + },
> + onWidgetAttach: 'onFileNameWidgetAttach',
> + },
> + {
> + header: gettext('File size'),
> + dataIndex: 'size',
> + },
> + {
> + header: gettext('MIME type'),
> + dataIndex: 'mimetype',
> + hidden: true,
> + },
> + {
> + xtype: 'actioncolumn',
> + items: [{
> + iconCls: 'fa critical fa-trash-o',
> + handler: 'removeFileHandler',
> + }],
> + width: 50,
> + },
> + {
> + header: gettext('Hash'),
> + dataIndex: 'hash',
> + width: 110,
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'pveHashAlgorithmSelector',
> + listeners: {
> + change: 'hashChange',
> + },
> + },
> + onWidgetAttach: 'onHashWidgetAttach',
> + },
> + {
> + header: gettext('Hash Value'),
> + dataIndex: 'hashsum',
> + renderer: data => data || 'None',
> + width: 300,
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'textfield',
> + disabled: true,
> + listeners: {
> + change: 'hashValueChange',
> + validityChange: 'hashValueValidityChange',
> + },
> + allowBlank: false,
> + },
> + onWidgetAttach: 'onHashValueWidgetAttach',
> + },
> + ],
> + },
> + ],
> +
> + buttons: [
> + {
> + xtype: 'filefield',
> + name: 'file',
> + buttonText: gettext('Add File'),
> + allowBlank: false,
> + hideLabel: true,
> + fieldStyle: 'display: none;',
> + cbind: {
> + accept: '{extensions}',
> + },
> + listeners: {
> + change: 'addFile',
> + render: 'enableUploadOfMultipleFiles',
> + },
> + },
> + {
> + xtype: 'button',
> + text: gettext('Cancel'),
> + handler: function() {
> + const me = this;
> + me.up('pveStorageUpload').close();
> + },
> + },
> + {
> + text: gettext('Start upload'),
> + handler: 'openUploadWindow',
> + bind: {
> + disabled: '{numFiles == 0 || !(validFiles === numFiles) || invalidHash}',
> + },
> + },
> + ],
> +});
> +
> +Ext.define('PVE.window.UploadProgress', {
> + extend: 'Ext.window.Window',
> + alias: 'widget.pveStorageUploadProgress',
> + mixins: ['Proxmox.Mixin.CBind'],
> + height: 400,
> + width: 800,
> + resizable: true,
> + scrollable: true,
> + modal: true,
> +
> + cbindData: function(initialConfig) {
> + const me = this;
> + const ext = me.acceptedExtensions[me.content] || [];
> +
> + me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
> +
> + return {
> + extensions: ext.join(', '),
> + filenameRegex: RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
> + store: initialConfig.store,
> + };
> + },
> +
> +
> + title: gettext('Upload Progress'),
> +
> + acceptedExtensions: {
> + iso: ['.img', '.iso'],
> + vztmpl: ['.tar.gz', '.tar.xz'],
> + },
> +
> + viewModel: {
> + data: {
> + numUploaded: 0,
> + numFiles: 0,
> + currentTask: '',
> + },
> +
> + formulas: {
> + loadingLabel: function(get) {
> + if (get('currentTask') === 'Copy files') {
> + return 'x-grid-row-loading';
> }
> - pbar.updateProgress(per, text);
> - };
> + return '';
> + },
> + },
> + },
> + controller: {
> + init: function(view) {
> + const me = this;
> + me.getViewModel().data.numFiles = view.numFiles;
> + me.startUpload();
> + },
> +
> + currentUploadIndex: 0,
> + startUpload: function() {
> + const me = this;
> + const view = me.getView();
> + const grid = me.lookup('grid');
> + const vm = me.getViewModel();
> +
> + const record = grid.store.getAt(me.currentUploadIndex++);
> + if (!record) {
> + vm.set('currentTask', 'Done');
> + 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) {
> + vm.set('currentTask', 'Copy files');
> if (xhr.status === 200) {
> - view.hide();
> -
> const result = JSON.parse(xhr.response);
> const upid = result.data;
> - Ext.create('Proxmox.window.TaskViewer', {
> - autoShow: true,
> + const taskviewer = Ext.create('Proxmox.window.TaskViewer', {
> upid: upid,
> - taskDone: view.taskDone,
> - listeners: {
> - destroy: function() {
> - view.close();
> - },
> + taskDone: function(success) {
> + vm.set('numUploaded', vm.get('numUploaded') + 1);
> + if (success) {
> + record.set('done', true);
> + } else {
> + const widget = record.get('progressWidget');
> + widget.updateProgress(0, "ERROR");
> + widget.setStyle('background-color', 'red');
> + }
> + view.taskDone();
> + me.startUpload();
> },
> + closeAction: 'hide',
> });
> + record.set('taskviewer', taskviewer);
> + record.get('taskViewerButton').enable();
> + } else {
> + const widget = record.get('progressWidget');
> + widget.updateProgress(0, `ERROR: ${xhr.status}`);
> + widget.setStyle('background-color', 'red');
> + me.startUpload();
> + }
> + });
>
> - 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);
> + record.get('progressWidget').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) {
> @@ -125,171 +453,102 @@ Ext.define('PVE.window.UploadToStorage', {
>
> xhr.open("POST", `/api2/json${view.url}`, true);
> xhr.send(fd);
> + vm.set('currentTask', 'Upload files');
> },
>
> - validitychange: function(f, valid) {
> - const submitBtn = this.lookup('submitBtn');
> - submitBtn.setDisabled(!valid);
> + onProgressWidgetAttach: function(col, widget, rec) {
> + rec.set('progressWidget', widget);
> + widget.updateProgress(0, "");
> },
>
> - 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) || '-');
> + onWindowClose: function(panel) {
> + const store = panel.lookup('grid').store;
> + store.each(record => record.get('xhr')?.abort());
> },
>
> - hashChange: function(field, value) {
> - const checksum = this.lookup('downloadUrlChecksum');
> - if (value === '__default__') {
> - checksum.setDisabled(true);
> - checksum.setValue("");
> - } else {
> - checksum.setDisabled(false);
> - }
> + showTaskViewer: function(button) {
> + button.record.get('taskviewer').show();
> + },
> +
> + onTaskButtonAttach: function(col, widget, rec) {
> + widget.record = rec;
> + rec.set('taskViewerButton', widget);
> },
> },
>
> items: [
> {
> - xtype: 'form',
> - reference: 'formPanel',
> - method: 'POST',
> - waitMsgTarget: true,
> - bodyPadding: 10,
> - border: false,
> - width: 400,
> - fieldDefaults: {
> - labelWidth: 100,
> - anchor: '100%',
> - },
> - items: [
> + xtype: 'grid',
> + reference: 'grid',
> +
> + cbind: {
> + store: '{store}',
> + },
> + columns: [
> {
> - xtype: 'filefield',
> - name: 'file',
> - buttonText: gettext('Select File'),
> - allowBlank: false,
> - fieldLabel: gettext('File'),
> - cbind: {
> - accept: '{extensions}',
> - },
> - listeners: {
> - change: 'fileChange',
> - },
> + header: gettext('File Name'),
> + dataIndex: 'filename',
> + flex: 4,
> },
> {
> - xtype: 'textfield',
> - name: 'filename',
> - allowBlank: false,
> - fieldLabel: gettext('File name'),
> - bind: {
> - value: '{filename}',
> - },
> - cbind: {
> - regex: '{filenameRegex}',
> + header: gettext('Progress Bar'),
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'progressbar',
> },
> - regexText: gettext('Wrong file extension'),
> + onWidgetAttach: 'onProgressWidgetAttach',
> + flex: 2,
> },
> {
> - xtype: 'displayfield',
> - name: 'size',
> - fieldLabel: gettext('File size'),
> - bind: {
> - value: '{size}',
> + header: gettext('Task Viewer'),
> + xtype: 'widgetcolumn',
> + widget: {
> + xtype: 'bu tton',
> + handler: 'showTaskViewer',
> + disabled: true,
> + text: 'Show',
> },
> + onWidgetAttach: 'onTaskButtonAttach',
> },
> + ],
> + tbar: [
> {
> xtype: 'displayfield',
> - name: 'mimetype',
> - fieldLabel: gettext('MIME type'),
> bind: {
> - value: '{mimetype}',
> + value: 'Files uploaded: {numUploaded} / {numFiles}',
> },
> },
> + '->',
> {
> - xtype: 'pveHashAlgorithmSelector',
> - name: 'checksum-algorithm',
> - fieldLabel: gettext('Hash algorithm'),
> - allowBlank: true,
> - hasNoneOption: true,
> - value: '__default__',
> - listeners: {
> - change: 'hashChange',
> + xtype: 'displayfield',
> + bind: {
> + value: '{currentTask}',
> },
> },
> {
> - xtype: 'textfield',
> - name: 'checksum',
> - fieldLabel: gettext('Checksum'),
> - allowBlank: false,
> - disabled: true,
> - emptyText: gettext('none'),
> - reference: 'downloadUrlChecksum',
> - },
> - {
> - xtype: 'progressbar',
> - text: 'Ready',
> - hidden: true,
> - reference: 'progressBar',
> - },
> - {
> - xtype: 'hiddenfield',
> - name: 'content',
> - cbind: {
> - value: '{content}',
> + xtype: 'displayfield',
> + userCls: 'x-grid-row-loading',
> + width: 30,
> + bind: {
> + hidden: '{currentTask === "Done"}',
> },
> },
> ],
> - listeners: {
> - validitychange: 'validitychange',
> - },
> },
> ],
>
> buttons: [
> {
> xtype: 'button',
> - text: gettext('Abort'),
> - reference: 'abortBtn',
> - disabled: true,
> + text: gettext('Exit'),
> handler: function() {
> const me = this;
> - me.up('pveStorageUpload').close();
> + me.up('pveStorageUploadProgress').close();
> },
> },
> - {
> - text: gettext('Upload'),
> - reference: 'submitBtn',
> - disabled: true,
> - handler: 'submit',
> - },
> ],
>
> 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();
> + close: 'onWindowClose',
> },
> });
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2023-03-31 11:34 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-07-20 12:26 [pve-devel] [PATCH v2 manager] fix #3248: GUI: storage: upload multiple files Matthias Heiserer
2022-07-29 9:29 ` Markus Frank
2023-03-31 11:33 ` Matthias Heiserer
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox