mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
⛄ Sync 2026-07-31 00:26:24
This commit is contained in:
1 parent
47d0743808
commit
b44a4364c4
125 files changed
+3954
-140418
No files matched your search
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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
|
||||
});
|
||||
Reference in new issue
Block a user