public inbox for pve-devel@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal