From: Mauro de Pascale <mauro.depascale.work@outlook.it>
To: "pve-devel@lists.proxmox.com" <pve-devel@lists.proxmox.com>
Subject: [PATCH 2/2] manager: add GUI support for backup export and import
Date: Wed, 23 Sep 2026 12:25:12 +0000 [thread overview]
Message-ID: <LOBP265MB89616B1DF4396ADDD9A8FC7DAD822@LOBP265MB8961.GBRP265.PROD.OUTLOOK.COM> (raw)
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
next reply other threads:[~2026-09-28 7:15 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-23 12:25 Mauro de Pascale [this message]
-- strict thread matches above, loose matches on Subject: below --
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 2/2] manager: add GUI support for backup export and import Mauro de Pascale
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=LOBP265MB89616B1DF4396ADDD9A8FC7DAD822@LOBP265MB8961.GBRP265.PROD.OUTLOOK.COM \
--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