|
@@ -1,310 +0,0 @@
|
|
|
-From 58a69ea37f94b9cfae8f4619627469a6596474cd Mon Sep 17 00:00:00 2001
|
|
|
-From: Paul Donald <[email protected]>
|
|
|
-Date: Fri, 11 Oct 2024 00:48:27 +0200
|
|
|
-Subject: [PATCH] luci-mod-system: Allow selective file restore from backups
|
|
|
-
|
|
|
-This is useful when you're importing backups intended for different
|
|
|
-hardware, for example, where e.g. `/etc/config/[network|wireless]` are
|
|
|
-not suitable for the target platform, but everything else useful.
|
|
|
-
|
|
|
-Caveats: subsequent extraction and compression steps happen on the
|
|
|
-target device, to then feed the selective_backup archive to sysupgrade.
|
|
|
-
|
|
|
-Signed-off-by: Paul Donald <[email protected]>
|
|
|
----
|
|
|
- .../resources/view/system/flash.js | 171 ++++++++++++++++++
|
|
|
- .../usr/share/rpcd/acl.d/luci-mod-system.json | 5 +
|
|
|
- 2 files changed, 176 insertions(+)
|
|
|
-
|
|
|
-diff --git a/feeds/luci/modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js b/feeds/luci/modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js
|
|
|
-index 2ef096f5f23f..3ffef2aba752 100644
|
|
|
---- a/feeds/luci/modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js
|
|
|
-+++ b/feeds/luci/modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js
|
|
|
-@@ -139,6 +139,85 @@ return view.extend({
|
|
|
- }, this, ev.target));
|
|
|
- },
|
|
|
-
|
|
|
-+ handleSelectiveRestore: function(ev) {
|
|
|
-+ return ui.uploadFile('/tmp/backup.tar.gz', ev.target)
|
|
|
-+ .then(L.bind(function(btn, res) {
|
|
|
-+ btn.firstChild.data = _('Checking archive…');
|
|
|
-+ return fs.exec('/bin/tar', [ '-tzf', '/tmp/backup.tar.gz' ]);
|
|
|
-+ }, this, ev.target))
|
|
|
-+ .then(L.bind(function(btn, res) {
|
|
|
-+ if (res.code != 0) {
|
|
|
-+ ui.addNotification(null, E('p', _('The uploaded backup archive is not readable')));
|
|
|
-+ return fs.remove('/tmp/backup.tar.gz');
|
|
|
-+ }
|
|
|
-+
|
|
|
-+ // Split the stdout to get individual file names
|
|
|
-+ var files = res.stdout.split('\n').filter(function(f) { return f.trim() !== ''; });
|
|
|
-+
|
|
|
-+ // Generate check-boxes for each file with <br /> after each
|
|
|
-+ var fileCheckboxes = files.map(function(file) {
|
|
|
-+ return E('label', { 'class': 'checkbox' }, [
|
|
|
-+ E('input', { 'type': 'checkbox', 'name': 'file', 'value': file }),
|
|
|
-+ ' ', file, E('br') // Add line break after each checkbox
|
|
|
-+ ]);
|
|
|
-+ });
|
|
|
-+
|
|
|
-+ // Helper buttons to check all, none, or inverse
|
|
|
-+ var checkAllButton = E('button', {
|
|
|
-+ 'class': 'btn',
|
|
|
-+ 'click': function() {
|
|
|
-+ // Select all check-boxes
|
|
|
-+ document.querySelectorAll('input[name="file"]').forEach(function(cb) {
|
|
|
-+ cb.checked = true;
|
|
|
-+ });
|
|
|
-+ }
|
|
|
-+ }, _('Check All'));
|
|
|
-+
|
|
|
-+ var uncheckAllButton = E('button', {
|
|
|
-+ 'class': 'btn',
|
|
|
-+ 'click': function() {
|
|
|
-+ // Deselect all check-boxes
|
|
|
-+ document.querySelectorAll('input[name="file"]').forEach(function(cb) {
|
|
|
-+ cb.checked = false;
|
|
|
-+ });
|
|
|
-+ }
|
|
|
-+ }, _('Check None'));
|
|
|
-+
|
|
|
-+ var invertButton = E('button', {
|
|
|
-+ 'class': 'btn',
|
|
|
-+ 'click': function() {
|
|
|
-+ // Deselect all check-boxes
|
|
|
-+ document.querySelectorAll('input[name="file"]').forEach(function(cb) {
|
|
|
-+ cb.checked = !cb.checked;
|
|
|
-+ });
|
|
|
-+ }
|
|
|
-+ }, _('Invert'));
|
|
|
-+
|
|
|
-+ // Display the modal with check-boxes and helper buttons
|
|
|
-+ ui.showModal(_('Apply backup?'), [
|
|
|
-+ E('p', _('The uploaded backup archive appears to be valid and contains the files listed below. Select the files you wish to restore and press "Continue", or "Cancel" to abort.')),
|
|
|
-+ E('div', {}, fileCheckboxes), // Display check-boxes for files
|
|
|
-+ E('div', { 'class': 'right' }, [
|
|
|
-+ checkAllButton, ' ', uncheckAllButton, ' ', invertButton, ' ',
|
|
|
-+ E('button', {
|
|
|
-+ 'class': 'btn',
|
|
|
-+ 'click': ui.createHandlerFn(this, function(ev) {
|
|
|
-+ return fs.remove('/tmp/backup.tar.gz').finally(ui.hideModal);
|
|
|
-+ })
|
|
|
-+ }, [ _('Cancel') ]), ' ',
|
|
|
-+ E('button', {
|
|
|
-+ 'class': 'btn cbi-button-action important',
|
|
|
-+ 'click': ui.createHandlerFn(this, 'handleSelectiveRestoreConfirm', btn)
|
|
|
-+ }, [ _('Continue') ])
|
|
|
-+ ])
|
|
|
-+ ]);
|
|
|
-+ }, this, ev.target))
|
|
|
-+ .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
|
|
|
-+ .finally(L.bind(function(btn, input) {
|
|
|
-+ btn.firstChild.data = _('Upload archive...');
|
|
|
-+ }, this, ev.target));
|
|
|
-+ },
|
|
|
-+
|
|
|
- handleRestoreConfirm: function(btn, ev) {
|
|
|
- return fs.exec('/sbin/sysupgrade', [ '--restore-backup', '/tmp/backup.tar.gz' ])
|
|
|
- .then(L.bind(function(btn, res) {
|
|
|
-@@ -169,6 +248,94 @@ return view.extend({
|
|
|
- .finally(function() { btn.firstChild.data = _('Upload archive...') });
|
|
|
- },
|
|
|
-
|
|
|
-+ handleSelectiveRestoreConfirm: function(btn, ev) {
|
|
|
-+ // Get the selected files from the check-boxes
|
|
|
-+ var selectedFiles = Array.prototype.slice.call(document.querySelectorAll('input[name="file"]:checked'))
|
|
|
-+ .map(function(checkbox) { return checkbox.value; });
|
|
|
-+
|
|
|
-+ if (selectedFiles.length === 0) {
|
|
|
-+ ui.addNotification(null, E('p', _('No files selected for restoration')));
|
|
|
-+ return fs.remove('/tmp/backup.tar.gz').finally(ui.hideModal);
|
|
|
-+ }
|
|
|
-+
|
|
|
-+ btn.firstChild.data = _('Creating archive of selected files…');
|
|
|
-+
|
|
|
-+ // Write the selected file names into a temporary file for tar -T option
|
|
|
-+ var fileList = selectedFiles.join('\n');
|
|
|
-+ return fs.write('/tmp/filelist.txt', fileList)
|
|
|
-+ .then(L.bind(function() {
|
|
|
-+ // Make selective restore directory
|
|
|
-+ return fs.exec('/bin/mkdir', ['-p', '/tmp/selective_restore/']);
|
|
|
-+ }, this))
|
|
|
-+ .then(L.bind(function(mkdirRes) {
|
|
|
-+ if (mkdirRes.code != 0) {
|
|
|
-+ ui.addNotification(null, [
|
|
|
-+ E('p', _('Path creation failed with code %d').format(mkdirRes.code)),
|
|
|
-+ mkdirRes.stderr ? E('pre', {}, [ mkdirRes.stderr ]) : ''
|
|
|
-+ ]);
|
|
|
-+ L.raise('Error', 'Path creation failed');
|
|
|
-+ }
|
|
|
-+ // Extract the tar.gz with only the selected files
|
|
|
-+ return fs.exec('/bin/tar', ['-xzf', '/tmp/backup.tar.gz', '-T', '/tmp/filelist.txt', '-C', '/tmp/selective_restore/']);
|
|
|
-+ }, this))
|
|
|
-+ .then(L.bind(function(tarRes) {
|
|
|
-+ if (tarRes.code != 0) {
|
|
|
-+ ui.addNotification(null, [
|
|
|
-+ E('p', _('Tar extraction failed with code %d').format(tarRes.code)),
|
|
|
-+ tarRes.stderr ? E('pre', {}, [ tarRes.stderr ]) : ''
|
|
|
-+ ]);
|
|
|
-+ L.raise('Error', 'Tar extraction failed');
|
|
|
-+ }
|
|
|
-+ // Create the new tar.gz with only the selected files
|
|
|
-+ return fs.exec('/bin/tar', ['-czf', '/tmp/selective_restore.tar.gz', '-T', '/tmp/filelist.txt', '-C', '/tmp/selective_restore/']);
|
|
|
-+ }, this))
|
|
|
-+ .then(L.bind(function(tarRes) {
|
|
|
-+ if (tarRes.code != 0) {
|
|
|
-+ ui.addNotification(null, [
|
|
|
-+ E('p', _('The tar creation failed with code %d').format(tarRes.code)),
|
|
|
-+ tarRes.stderr ? E('pre', {}, [ tarRes.stderr ]) : ''
|
|
|
-+ ]);
|
|
|
-+ L.raise('Error', 'Tar creation failed');
|
|
|
-+ }
|
|
|
-+
|
|
|
-+ // Now use sysupgrade with the newly created tar.gz
|
|
|
-+ return fs.exec('/sbin/sysupgrade', ['--restore-backup', '/tmp/selective_restore.tar.gz']);
|
|
|
-+ }, this))
|
|
|
-+ .then(L.bind(function(sysupgradeRes) {
|
|
|
-+ if (sysupgradeRes.code != 0) {
|
|
|
-+ ui.addNotification(null, [
|
|
|
-+ E('p', _('The sysupgrade command failed with code %d').format(sysupgradeRes.code)),
|
|
|
-+ sysupgradeRes.stderr ? E('pre', {}, [ sysupgradeRes.stderr ]) : ''
|
|
|
-+ ]);
|
|
|
-+ L.raise('Error', 'Sysupgrade failed');
|
|
|
-+ }
|
|
|
-+
|
|
|
-+ // Proceed with reboot after successful restore
|
|
|
-+ btn.firstChild.data = _('Rebooting…');
|
|
|
-+ return fs.exec('/sbin/reboot');
|
|
|
-+ }, this))
|
|
|
-+ .then(L.bind(function(res) {
|
|
|
-+ if (res.code != 0) {
|
|
|
-+ ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res.code)));
|
|
|
-+ L.raise('Error', 'Reboot failed');
|
|
|
-+ }
|
|
|
-+
|
|
|
-+ // Show rebooting modal
|
|
|
-+ ui.showModal(_('Rebooting…'), [
|
|
|
-+ E('p', { 'class': 'spinning' }, _('The system is rebooting now. If the restored configuration changed the current LAN IP address, you might need to reconnect manually.'))
|
|
|
-+ ]);
|
|
|
-+
|
|
|
-+ // Await reconnection
|
|
|
-+ ui.awaitReconnect(window.location.host, '192.168.1.1', 'openwrt.lan');
|
|
|
-+ }, this))
|
|
|
-+ .catch(function(e) {
|
|
|
-+ ui.addNotification(null, E('p', e.message));
|
|
|
-+ })
|
|
|
-+ .finally(function() {
|
|
|
-+ btn.firstChild.data = _('Upload archive...');
|
|
|
-+ });
|
|
|
-+ },
|
|
|
-+
|
|
|
- handleBlock: function(hostname, ev) {
|
|
|
- var mtdblock = dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select');
|
|
|
- var mtdnumber = mtdblock.value;
|
|
|
-@@ -414,6 +581,10 @@ return view.extend({
|
|
|
- o.inputtitle = _('Upload archive...');
|
|
|
- o.onclick = L.bind(this.handleRestore, this);
|
|
|
-
|
|
|
-+ o = ss.option(form.Button, 'selective_restore', _('Selectively Restore backup'), _('Custom files (certificates, scripts) may remain on the system. To prevent this, perform a factory-reset first.'));
|
|
|
-+ o.inputstyle = 'action important';
|
|
|
-+ o.inputtitle = _('Upload archive...');
|
|
|
-+ o.onclick = L.bind(this.handleSelectiveRestore, this);
|
|
|
-
|
|
|
- var mtdblocks = [];
|
|
|
- procmtd.split(/\n/).forEach(function(ln) {
|
|
|
-diff --git a/feeds/luci/modules/luci-mod-system/root/usr/share/rpcd/acl.d/luci-mod-system.json b/feeds/luci/modules/luci-mod-system/root/usr/share/rpcd/acl.d/luci-mod-system.json
|
|
|
-index b096d870a754..2991613ef1af 100644
|
|
|
---- a/feeds/luci/modules/luci-mod-system/root/usr/share/rpcd/acl.d/luci-mod-system.json
|
|
|
-+++ b/feeds/luci/modules/luci-mod-system/root/usr/share/rpcd/acl.d/luci-mod-system.json
|
|
|
-@@ -149,6 +149,9 @@
|
|
|
- "cgi-io": [ "upload" ],
|
|
|
- "file": {
|
|
|
- "/bin/tar -tzf /tmp/backup.tar.gz": [ "exec" ],
|
|
|
-+ "/bin/mkdir -p /tmp/selective_restore/": [ "exec" ],
|
|
|
-+ "/bin/tar -xzf /tmp/backup.tar.gz -T /tmp/filelist.txt -C /tmp/selective_restore/": [ "exec" ],
|
|
|
-+ "/bin/tar -czf /tmp/selective_restore.tar.gz -T /tmp/filelist.txt -C /tmp/selective_restore/": [ "exec" ],
|
|
|
- "/etc/sysupgrade.conf": [ "write" ],
|
|
|
- "/sbin/firstboot -r -y": [ "exec" ],
|
|
|
- "/sbin/reboot": [ "exec" ],
|
|
|
-@@ -162,8 +165,10 @@
|
|
|
- "/sbin/sysupgrade -u /tmp/firmware.bin": [ "exec" ],
|
|
|
- "/sbin/sysupgrade -u -k /tmp/firmware.bin": [ "exec" ],
|
|
|
- "/sbin/sysupgrade --restore-backup /tmp/backup.tar.gz": [ "exec" ],
|
|
|
-+ "/sbin/sysupgrade --restore-backup /tmp/selective_restore.tar.gz": [ "exec" ],
|
|
|
- "/sbin/sysupgrade --test /tmp/firmware.bin": [ "exec" ],
|
|
|
- "/sbin/sysupgrade /tmp/firmware.bin": [ "exec" ],
|
|
|
-+ "/tmp/filelist.txt": [ "write" ],
|
|
|
- "/tmp/backup.tar.gz": [ "write" ],
|
|
|
- "/tmp/firmware.bin": [ "write" ]
|
|
|
- },
|
|
|
-diff --git a/package/base-files/files/sbin/sysupgrade b/package/base-files/files/sbin/sysupgrade
|
|
|
-index 75817d178aea18..4a51304f771e20 100755
|
|
|
---- a/package/base-files/files/sbin/sysupgrade
|
|
|
-+++ b/package/base-files/files/sbin/sysupgrade
|
|
|
-@@ -22,6 +22,8 @@ CONF_BACKUP_LIST=0
|
|
|
- CONF_BACKUP_LIST=0
|
|
|
- CONF_BACKUP=
|
|
|
- CONF_RESTORE=
|
|
|
-+CONF_SELECTIVE_RESTORE=
|
|
|
-+CONF_SELECTIVE_RESTORE_FILELIST=
|
|
|
- NEED_IMAGE=
|
|
|
- HELP=0
|
|
|
- TEST=0
|
|
|
-@@ -51,6 +53,7 @@ while [ -n "$1" ]; do
|
|
|
- -u) SKIP_UNCHANGED=1;;
|
|
|
- -b|--create-backup) CONF_BACKUP="$2" NEED_IMAGE=1; shift;;
|
|
|
- -r|--restore-backup) CONF_RESTORE="$2" NEED_IMAGE=1; shift;;
|
|
|
-+ -S|--selectively-restore-backup) CONF_SELECTIVE_RESTORE_FILELIST="$2" CONF_SELECTIVE_RESTORE="$3" NEED_IMAGE=1; shift;shift;;
|
|
|
- -l|--list-backup) CONF_BACKUP_LIST=1;;
|
|
|
- -f) CONF_IMAGE="$2"; shift;;
|
|
|
- -s) USE_CURR_PART=1;;
|
|
|
-@@ -105,6 +108,11 @@ backup-command:
|
|
|
- restore a .tar.gz created with sysupgrade -b
|
|
|
- then exit. Does not flash an image. If file is '-',
|
|
|
- the archive is read from stdin.
|
|
|
-+ -S | --selectively-restore-backup <file-with-list-of-filenames> <file>
|
|
|
-+ selectively restore a .tar.gz created with sysupgrade -b
|
|
|
-+ by passing a file containing the desired list of filenames,
|
|
|
-+ then exit. Does not flash an image. If file is '-',
|
|
|
-+ the archive is read from stdin.
|
|
|
- -l | --list-backup
|
|
|
- list the files that would be backed up when calling
|
|
|
- sysupgrade -b. Does not create a backup file.
|
|
|
-@@ -351,6 +359,22 @@ if [ -n "$CONF_RESTORE" ]; then
|
|
|
- exit $?
|
|
|
- fi
|
|
|
-
|
|
|
-+if [ -n "$CONF_SELECTIVE_RESTORE" ]; then
|
|
|
-+ if [ "$CONF_SELECTIVE_RESTORE" != "-" ] && [ ! -f "$CONF_SELECTIVE_RESTORE" ]; then
|
|
|
-+ echo "Backup archive '$CONF_SELECTIVE_RESTORE' not found." >&2
|
|
|
-+ exit 1
|
|
|
-+ fi
|
|
|
-+
|
|
|
-+ [ "$VERBOSE" -gt 1 ] && TAR_V="v" || TAR_V=""
|
|
|
-+ v "Selectively restoring config files..."
|
|
|
-+ if [ "$(type -t platform_restore_backup)" = 'platform_restore_backup' ]; then
|
|
|
-+ platform_selectively_restore_backup "$TAR_V"
|
|
|
-+ else
|
|
|
-+ tar -C / -x"${TAR_V}"zf "$CONF_SELECTIVE_RESTORE" -T "$CONF_SELECTIVE_RESTORE_FILELIST"
|
|
|
-+ fi
|
|
|
-+ exit $?
|
|
|
-+fi
|
|
|
-+
|
|
|
- type platform_check_image >/dev/null 2>/dev/null || {
|
|
|
- echo "Firmware upgrade is not implemented for this platform." >&2
|
|
|
- exit 1
|
|
|
-diff --git a/target/linux/bcm27xx/base-files/lib/upgrade/platform.sh b/target/linux/bcm27xx/base-files/lib/upgrade/platform.sh
|
|
|
-index 69cc60e2bcb095..b7fffd68b4b450 100644
|
|
|
---- a/target/linux/bcm27xx/base-files/lib/upgrade/platform.sh
|
|
|
-+++ b/target/linux/bcm27xx/base-files/lib/upgrade/platform.sh
|
|
|
-@@ -128,3 +128,10 @@ platform_restore_backup() {
|
|
|
- tar -C / -x${TAR_V}zf "$CONF_RESTORE"
|
|
|
- bcm27xx_set_root_part
|
|
|
- }
|
|
|
-+
|
|
|
-+platform_selectively_restore_backup() {
|
|
|
-+ local TAR_V=$1
|
|
|
-+
|
|
|
-+ tar -C / -x${TAR_V}zf "$CONF_RESTORE" -T "$CONF_SELECTIVE_RESTORE_FILELIST"
|
|
|
-+ bcm27xx_set_root_part
|
|
|
-+}
|