all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Mauro de Pascale <mauro.depascale.work@outlook.it>
To: "pve-devel@lists.proxmox.com" <pve-devel@lists.proxmox.com>
Cc: Mauro de Pascale <mauro.depascale.work@outlook.it>
Subject: [PATCH 2/2] manager: add GUI support for backup export and import
Date: Fri, 26 Jun 2026 16:43:59 +0000	[thread overview]
Message-ID: <20260626164354.44747-3-mauro.depascale.work@outlook.it> (raw)
In-Reply-To: <20260626164354.44747-1-mauro.depascale.work@outlook.it>

From: Mauro de Pascale <mauro.depascale.work@outlook.it>

Expose backup export from the backup dialog and allow uploading VMA
archives for restore directly from the storage view.
---
 PVE/API2/VZDump.pm                     |  2 +-
 www/manager6/Utils.js                  | 16 +++----
 www/manager6/grid/BackupView.js        | 24 +++++++++-
 www/manager6/storage/BackupView.js     | 29 +++++++++++-
 www/manager6/window/Backup.js          | 33 ++++++++++++-
 www/manager6/window/UploadToStorage.js | 65 ++++++++++++++++++++++++--
 6 files changed, 149 insertions(+), 20 deletions(-)

diff --git a/PVE/API2/VZDump.pm b/PVE/API2/VZDump.pm
index 84f42352..45c316c1 100644
--- a/PVE/API2/VZDump.pm
+++ b/PVE/API2/VZDump.pm
@@ -351,7 +351,7 @@ __PACKAGE__->register_method({
                     : $param->{compress} eq 'gzip' ? 'gz'
                     : $param->{compress} eq 'lzo'  ? 'lzo'
                     : 'vma';
-       
+
 	my $filename = "vzdump-qemu-$vmid.vma.$suffix";
 
 	my $cmd = ['/usr/bin/vzdump', $vmid, '--stdout','1','--compress',$compress];
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 040b5ae0..412fba0a 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -145,7 +145,7 @@ Ext.define('PVE.Utils', {
                 bvers = b.toString().split('.');
             }
 
-            for (;;) {
+            for (; ;) {
                 let av = avers.shift();
                 let bv = bvers.shift();
 
@@ -1272,8 +1272,7 @@ Ext.define('PVE.Utils', {
         calculate_disk_usage: function (data) {
             if (
                 !Ext.isNumeric(data.disk) ||
-                data.type === 'qemu' ||
-                (data.type === 'lxc' && data.uptime === 0) ||
+                ((data.type === 'qemu' || data.type === 'lxc') && data.uptime === 0) ||
                 data.maxdisk === 0
             ) {
                 return -1;
@@ -1298,8 +1297,7 @@ Ext.define('PVE.Utils', {
             if (
                 !Ext.isNumeric(disk) ||
                 maxdisk === 0 ||
-                type === 'qemu' ||
-                (type === 'lxc' && record.data.uptime === 0)
+                ((type === 'qemu' || type === 'lxc') && record.data.uptime === 0)
             ) {
                 return '';
             }
@@ -1810,9 +1808,6 @@ Ext.define('PVE.Utils', {
         qemu_min_version: function (toCheck, minVersion) {
             let i;
             for (i = 0; i < toCheck.length && i < minVersion.length; i++) {
-                if (toCheck[i] > minVersion[i]) {
-                    return true;
-                }
                 if (toCheck[i] < minVersion[i]) {
                     return false;
                 }
@@ -1902,8 +1897,8 @@ Ext.define('PVE.Utils', {
                     container.mask(
                         Ext.String.format(
                             gettext('{0} not installed.') +
-                                ' ' +
-                                gettext('Log in as root to install.'),
+                            ' ' +
+                            gettext('Log in as root to install.'),
                             'Ceph',
                         ),
                         ['pve-static-mask'],
@@ -2185,6 +2180,7 @@ Ext.define('PVE.Utils', {
             hastop: ['HA', gettext('Stop')],
             imgcopy: ['', gettext('Copy data')],
             imgdel: ['', gettext('Erase data')],
+            importvm: ['VM', gettext('Importing')],
             lvmcreate: [gettext('LVM Storage'), gettext('Create')],
             lvmremove: ['Volume Group', gettext('Remove')],
             lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
diff --git a/www/manager6/grid/BackupView.js b/www/manager6/grid/BackupView.js
index 406143dc..e63c7bdc 100644
--- a/www/manager6/grid/BackupView.js
+++ b/www/manager6/grid/BackupView.js
@@ -253,6 +253,27 @@ Ext.define('PVE.grid.BackupView', {
             },
         });
 
+        let import_btn = Ext.create('Proxmox.button.Button', {
+            text: 'Import',
+            iconCls: 'fa fa-upload',
+            tooltip: gettext('Import backup and restore VM from local machine'),
+            handler: function () {
+                Ext.create('PVE.window.UploadToStorage', {
+                    nodename: nodename,
+                    storage: storagesel.getValue(),
+                    requiredPermissions: {
+                        check: ['Datastore.Allocate', 'VM.Allocate'],
+                    },
+                    content: 'import',
+                    listeners: {
+                        destroy: () => reload(),
+                    },
+                    autoShow: true,
+                });
+            },
+        });
+
+
         Ext.apply(me, {
             selModel: sm,
             tbar: {
@@ -260,6 +281,8 @@ Ext.define('PVE.grid.BackupView', {
                 items: [
                     backup_btn,
                     '-',
+                    import_btn,
+                    '-',
                     restore_btn,
                     file_restore_btn,
                     config_btn,
@@ -376,7 +399,6 @@ Ext.define('PVE.grid.BackupView', {
                     renderer: PVE.Utils.render_backup_encryption,
                 },
                 {
-                    // TRANSLATORS: The state of the verification task
                     header: gettext('Verify State'),
                     dataIndex: 'verification',
                     renderer: PVE.Utils.render_backup_verification,
diff --git a/www/manager6/storage/BackupView.js b/www/manager6/storage/BackupView.js
index 8c91ec99..f36030b9 100644
--- a/www/manager6/storage/BackupView.js
+++ b/www/manager6/storage/BackupView.js
@@ -10,6 +10,9 @@ Ext.define('PVE.storage.BackupView', {
     initComponent: function () {
         let me = this;
 
+        let content = me.pveSelNode.data.content?.split(',') ?? [];
+        me.hasBackupContent = content.includes('backup');
+
         let nodename = (me.nodename = me.pveSelNode.data.node);
         if (!nodename) {
             throw 'no node name specified';
@@ -79,6 +82,30 @@ Ext.define('PVE.storage.BackupView', {
         let isPBS = me.pluginType === 'pbs';
 
         me.tbar = [
+            {
+                xtype: 'proxmoxButton',
+                text: gettext('Import'),
+                iconCls: 'fa fa-upload',
+                //disabled: false,
+                selModel: null,
+                disabled: !me.hasBackupContent,
+                requiredPermissions: {
+                    check: ['Datastore.Allocate', 'VM.Allocate'],
+                },
+                tooltip: gettext('Import backup and restore VM from local machine'),
+                handler: function () {
+                    let win = Ext.create('PVE.window.UploadToStorage', {
+                        nodename: me.nodename,
+                        storage: me.storage,
+                        content: 'import',
+                        listeners: {
+                            destroy: () => me.store.load(),
+                        },
+                    });
+                    win.show();
+                },
+            },
+            '-',
             {
                 xtype: 'proxmoxButton',
                 text: gettext('Restore'),
@@ -106,7 +133,7 @@ Ext.define('PVE.storage.BackupView', {
                         },
                     });
                 },
-            },
+            }
         ];
         if (isPBS) {
             me.tbar.push({
diff --git a/www/manager6/window/Backup.js b/www/manager6/window/Backup.js
index 7c1c54de..d8837d6b 100644
--- a/www/manager6/window/Backup.js
+++ b/www/manager6/window/Backup.js
@@ -41,7 +41,6 @@ Ext.define('PVE.window.Backup', {
             xtype: 'proxmoxKVComboBox',
             comboItems: [
                 ['notification-system', gettext('Use global settings')],
-                // TRANSLATORS: sendmail is a piece of software
                 ['legacy-sendmail', gettext('Use sendmail (legacy)')],
             ],
             fieldLabel: gettext('Notification'),
@@ -245,11 +244,34 @@ Ext.define('PVE.window.Backup', {
             padding: '0 0 1 0',
         });
 
+        let exportCheckbox = Ext.create('Proxmox.form.Checkbox', {
+            name: 'export',
+            checked: false,
+            uncheckedValue: 0,
+            tooltip: gettext('If selected Backup is saved to local machine'),
+            fieldLabel: gettext('Export'),
+            // Tiny amount of padding to stop the UI from shifting
+            // when the 'mailto' field is shown.
+            padding: '0 0 1 0',
+            listeners: {
+                change: function (f, checked) {
+
+	            //console.log('clicked "export"');
+                    storagesel.setDisabled(checked);
+                    protectedCheckbox.setHidden(checked);
+		    notificationModeSelector.setDisabled(checked);
+                    if (checked) {
+                      protectedCheckbox.setValue(false);
+                    }
+                },
+            },
+        });
+
         me.formPanel = Ext.create('Proxmox.panel.InputPanel', {
             bodyPadding: 10,
             border: false,
             column1: [storagesel, modeSelector, protectedCheckbox, pbsChangeDetection],
-            column2: [compressionSelector, notificationModeSelector, mailtoField, removeCheckbox],
+            column2: [compressionSelector, notificationModeSelector, exportCheckbox, mailtoField, removeCheckbox],
             columnB: [
                 {
                     xtype: 'textareafield',
@@ -341,6 +363,13 @@ Ext.define('PVE.window.Backup', {
                     );
                 }
 
+                if (values.export) {
+                   let url = `/api2/json/nodes/${me.nodename}/vzdump/export?vmid=${me.vmid}&compress=${values.compress}`;
+                   window.open(url, '_blank');
+                   me.close();
+                   return;
+                }
+
                 Proxmox.Utils.API2Request({
                     url: '/nodes/' + me.nodename + '/vzdump',
                     params: params,
diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
index cc53596d..7d4bace9 100644
--- a/www/manager6/window/UploadToStorage.js
+++ b/www/manager6/window/UploadToStorage.js
@@ -9,7 +9,7 @@ Ext.define('PVE.window.UploadToStorage', {
     title: gettext('Upload'),
 
     acceptedExtensions: {
-        import: ['.ova', '.qcow2', '.raw', '.vmdk'],
+        import: ['.ova', '.qcow2', '.raw', '.vmdk', '.vma', '.vma.zst'],
         iso: ['.img', '.iso'],
         vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'],
     },
@@ -24,14 +24,15 @@ Ext.define('PVE.window.UploadToStorage', {
     cbindData: function (initialConfig) {
         const me = this;
         const ext = me.acceptedExtensions[me.content] || [];
-
+        let fRegex = new RegExp('^.*(?:' + ext.map(e => e.replace(/\./g, '\\.')).join('|') + ')$', 'i');
         me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
 
         let fileSelectorExt = ext.concat(Object.keys(me.extensionAliases[me.content] ?? {}));
 
+
         return {
             extensions: fileSelectorExt.join(', '),
-            filenameRegex: new RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
+            filenameRegex: fRegex,
         };
     },
 
@@ -45,6 +46,17 @@ Ext.define('PVE.window.UploadToStorage', {
 
     controller: {
         submit: function (button) {
+
+            let doUpload = function () {
+                xhr.open('POST', `/api2/json${url}`, true);
+
+                if (window.Proxmox && Proxmox.CSRFPreventionToken) {
+                    xhr.setRequestHeader('CSRFPreventionToken', Proxmox.CSRFPreventionToken);
+                }
+
+                xhr.send(fd);
+            };
+
             const view = this.getView();
             const form = this.lookup('formPanel').getForm();
             const abortBtn = this.lookup('abortBtn');
@@ -141,8 +153,49 @@ Ext.define('PVE.window.UploadToStorage', {
                 false,
             );
 
-            xhr.open('POST', `/api2/json${view.url}`, true);
-            xhr.send(fd);
+            let url = view.url;
+
+            if (view.content === 'import') {
+                let vmid = null;
+
+                // estrai vmid dal filename tipo vzdump-qemu-100.vma.zst
+                let match = filename.match(/-(\d+)\./);
+                if (match) {
+                    vmid = match[1];
+                }
+
+                if (vmid) {
+                    Proxmox.Utils.API2Request({
+                        url: `/nodes/${view.nodename}/qemu/${vmid}/status/current`,
+                        method: 'GET',
+
+                        success: function () {
+                            // VM esiste → chiedi conferma
+                            Ext.Msg.confirm(
+                                gettext('Confirm Restore'),
+                                Ext.String.format(
+                                    gettext('VM {0} already exists. Overwrite?'),
+                                    vmid
+                                ),
+                                function (btn) {
+                                    if (btn === 'yes') {
+                                        doUpload();
+                                    }
+                                }
+                            );
+                        },
+
+                        failure: function () {
+                            // VM non esiste → procedi direttamente
+                            doUpload();
+                        },
+                    });
+
+                    return; // blocca flusso normale
+                }
+            }
+
+            doUpload();
         },
 
         validitychange: function (f, valid) {
@@ -327,4 +380,6 @@ Ext.define('PVE.window.UploadToStorage', {
 
         me.callParent();
     },
+
+
 });
-- 
2.47.3


      parent reply	other threads:[~2026-07-01  8:05 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-26 16:43 [PATCH 0/2] RFC: manager: add direct VM backup export/import support Mauro de Pascale
2026-06-26 16:43 ` [PATCH 1/2] manager: add API endpoint for streamed VM backup export Mauro de Pascale
2026-06-26 16:43 ` Mauro de Pascale [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260626164354.44747-3-mauro.depascale.work@outlook.it \
    --to=mauro.depascale.work@outlook.it \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal