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
prev 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