mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-12 11:24:37 +08:00
222 lines
7.1 KiB
JavaScript
222 lines
7.1 KiB
JavaScript
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* Copyright (C) 2026 HomeProxy Custom
|
|
*/
|
|
|
|
'use strict';
|
|
'require fs';
|
|
'require rpc';
|
|
'require ui';
|
|
'require view';
|
|
|
|
// 当前页面会话里的动态路径状态
|
|
let currentBackupPath = null;
|
|
let currentRestorePath = null;
|
|
|
|
const callBackupCreate = rpc.declare({
|
|
object: 'luci.homeproxy_backup',
|
|
method: 'backup_create',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callBackupGetUploadPath = rpc.declare({
|
|
object: 'luci.homeproxy_backup',
|
|
method: 'backup_get_upload_path',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callBackupCleanup = rpc.declare({
|
|
object: 'luci.homeproxy_backup',
|
|
method: 'backup_cleanup',
|
|
params: ['path'],
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callBackupValidate = rpc.declare({
|
|
object: 'luci.homeproxy_backup',
|
|
method: 'backup_validate',
|
|
params: ['path'],
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callBackupRestore = rpc.declare({
|
|
object: 'luci.homeproxy_backup',
|
|
method: 'backup_restore',
|
|
params: ['path'],
|
|
expect: { '': {} }
|
|
});
|
|
|
|
function downloadFile(path, filename) {
|
|
return fs.read_direct(path, 'blob').then((blob) => {
|
|
const url = window.URL.createObjectURL(blob);
|
|
let a = document.createElement('a');
|
|
|
|
a.style.display = 'none';
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.setTimeout(() => {
|
|
a.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
}, 100);
|
|
});
|
|
}
|
|
|
|
function renderFileList(files) {
|
|
if (!files || !files.length)
|
|
return E('em', _('No files.'));
|
|
|
|
return E('ul', {}, files.map((file) => E('li', {}, file)));
|
|
}
|
|
|
|
function setButtonText(btn, text) {
|
|
if (btn && btn.firstChild)
|
|
btn.firstChild.data = text;
|
|
}
|
|
|
|
return view.extend({
|
|
handleCreateBackup(ev) {
|
|
const btn = ev.currentTarget || ev.target;
|
|
|
|
setButtonText(btn, _('Generating backup file...'));
|
|
|
|
return L.resolveDefault(callBackupCreate(), {}).then((res) => {
|
|
if (!res.result)
|
|
throw new Error(res.error || _('Failed to generate backup file.'));
|
|
|
|
// 记录后端生成的备份路径,供浏览器下载
|
|
currentBackupPath = res.download_path || res.path;
|
|
if (!currentBackupPath)
|
|
throw new Error(_('Backup file path was not returned.'));
|
|
|
|
const filename = 'homeproxy-backup-%s.tar.gz'.format((new Date()).toISOString().replace(/[:.]/g, '-'));
|
|
|
|
return downloadFile(currentBackupPath, filename).then(() => {
|
|
ui.addNotification(null, E('p', _('Backup file has been generated and download has started.')), 'info');
|
|
// 下载后清理后端临时备份文件,避免证书和私钥留在 /tmp
|
|
return L.resolveDefault(callBackupCleanup(currentBackupPath), {});
|
|
});
|
|
}).catch((err) => {
|
|
ui.addNotification(null, E('p', err.message || err));
|
|
}).finally(() => {
|
|
setButtonText(btn, _('Generate backup file'));
|
|
});
|
|
},
|
|
|
|
handleUploadRestore(ev) {
|
|
const btn = ev.currentTarget || ev.target;
|
|
|
|
setButtonText(btn, _('Uploading backup file...'));
|
|
|
|
// 先向后端申请本次上传路径
|
|
return L.resolveDefault(callBackupGetUploadPath(), {}).then((res) => {
|
|
if (!res.upload_path)
|
|
throw new Error(_('Failed to get upload path'));
|
|
|
|
currentRestorePath = res.upload_path;
|
|
|
|
return ui.uploadFile(currentRestorePath);
|
|
}).then(() => {
|
|
setButtonText(btn, _('Checking backup file...'));
|
|
return L.resolveDefault(callBackupValidate(currentRestorePath), {});
|
|
}).then((res) => {
|
|
if (!res.valid)
|
|
throw new Error(res.error || _('The uploaded backup file is invalid.'));
|
|
|
|
ui.showModal(_('Restore HomeProxy backup?'), [
|
|
E('p', _('The uploaded backup file contains the following HomeProxy source configuration. Restoring it will overwrite the current HomeProxy settings, custom direct/proxy lists, and uploaded certificates and private keys.')),
|
|
renderFileList(res.files),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.createHandlerFn(this, () => {
|
|
return L.resolveDefault(callBackupCleanup(currentRestorePath), {}).finally(ui.hideModal);
|
|
})
|
|
}, [ _('Cancel') ]),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action important',
|
|
'click': ui.createHandlerFn(this, 'handleRestoreConfirm')
|
|
}, [ _('Continue') ])
|
|
])
|
|
]);
|
|
}).catch((err) => {
|
|
ui.addNotification(null, E('p', err.message || err));
|
|
return L.resolveDefault(callBackupCleanup(currentRestorePath), {});
|
|
}).finally(() => {
|
|
setButtonText(btn, _('Upload backup file...'));
|
|
});
|
|
},
|
|
|
|
handleRestoreConfirm() {
|
|
ui.showModal(_('Restoring HomeProxy backup...'), [
|
|
E('p', { 'class': 'spinning' }, _('Applying backup file and restarting HomeProxy.'))
|
|
]);
|
|
|
|
return L.resolveDefault(callBackupRestore(currentRestorePath), {}).then((res) => {
|
|
if (!res.result)
|
|
throw new Error(res.error || _('Restore failed.'));
|
|
|
|
ui.showModal(_('Restore complete'), [
|
|
E('p', _('HomeProxy configuration has been restored and the service has restarted.')),
|
|
E('p', _('The rollback archive for the previous HomeProxy configuration has been saved to %s.').format(res.rollback || '/tmp/homeproxy-rollback.tar.gz')),
|
|
renderFileList(res.files),
|
|
E('div', { 'class': 'right' },
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, [ _('Close') ])
|
|
)
|
|
]);
|
|
}).catch((err) => {
|
|
ui.addNotification(null, E('p', err.message || err));
|
|
ui.hideModal();
|
|
});
|
|
},
|
|
|
|
render() {
|
|
const readonly = !L.hasViewPermission();
|
|
|
|
return E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', {}, _('HomeProxy configuration backup / restore')),
|
|
E('div', { 'class': 'cbi-map-descr' },
|
|
_('Export and import HomeProxy source configuration to fully restore WebUI settings after reinstallation.')),
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Backup')),
|
|
E('div', { 'class': 'cbi-section-descr' },
|
|
_('Download a tar.gz backup containing /etc/config/homeproxy, custom direct/proxy lists, and uploaded certificates and private keys. The backup contains plaintext private keys; keep it safe and avoid transmitting or storing it through untrusted channels. Restore only verifies file integrity (SHA-256 manifest), not source authenticity.')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Download backup')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action important',
|
|
'disabled': readonly || null,
|
|
'click': ui.createHandlerFn(this, 'handleCreateBackup')
|
|
}, [ _('Generate backup file') ])
|
|
])
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Restore')),
|
|
E('div', { 'class': 'cbi-section-descr' },
|
|
_('Upload a HomeProxy backup file and overwrite the current HomeProxy source configuration. A rollback archive is saved automatically before restore.')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Restore backup')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action important',
|
|
'disabled': readonly || null,
|
|
'click': ui.createHandlerFn(this, 'handleUploadRestore')
|
|
}, [ _('Upload backup file...') ])
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|