mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-01 16:44:45 +08:00
update 2026-09-06 01:51:51
This commit is contained in:
1 parent
47831f020d
commit
37eb246de8
1970 files changed
+1523980
-168
No files matched your search
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-daede
|
||||
PKG_VERSION:=1.14.7
|
||||
PKG_RELEASE:=24
|
||||
PKG_RELEASE:=25
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
@@ -78,6 +78,11 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/proxy-check.sh $(1)/usr/share/luci-app-daede/proxy-check.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/fetch-clash-yaml.sh $(1)/usr/share/luci-app-daede/fetch-clash-yaml.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/config-backup.sh $(1)/usr/share/luci-app-daede/config-backup.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/config-defaults.sh $(1)/usr/share/luci-app-daede/config-defaults.sh
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci-app-daede/defaults
|
||||
$(INSTALL_DATA) $(CURDIR)/../dae/files/dae.config $(1)/usr/share/luci-app-daede/defaults/dae
|
||||
$(INSTALL_DATA) $(CURDIR)/../daed/files/daed.config $(1)/usr/share/luci-app-daede/defaults/daed
|
||||
$(INSTALL_DATA) ./root/etc/config/daede $(1)/usr/share/luci-app-daede/defaults/daede
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/refresh-index.sh $(1)/usr/share/luci-app-daede/refresh-index.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/geo-cron.sh $(1)/usr/share/luci-app-daede/geo-cron.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/daed-sub-update.sh $(1)/usr/share/luci-app-daede/daed-sub-update.sh
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
'require view.daede.styles as styles';
|
||||
|
||||
const MAX_LINES = 5000;
|
||||
|
||||
@@ -54,6 +55,7 @@ const CSS = [
|
||||
/* 拆字段:time="May 25 07:04:59" level=info msg="..." key=val key="val with space" ... */
|
||||
const RE_LINE = /^time="([^"]*)"\s+level=(\w+)\s+msg=(?:"((?:[^"\\]|\\.)*)"|(\S+))\s*(.*)$/;
|
||||
const RE_PREFIXED_LINE = /^\[([^\]]+)\]\s+(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s*(.*)$/i;
|
||||
const RE_PLAIN_LEVEL_LINE = /^\s*(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s+(.*)$/i;
|
||||
|
||||
function detectLevel(line) {
|
||||
// daed/dae logs use lvl=info / [INFO] / level=warning style
|
||||
@@ -117,13 +119,21 @@ function parseLine(line) {
|
||||
}
|
||||
|
||||
m = line.match(RE_PREFIXED_LINE);
|
||||
if (!m) return null;
|
||||
|
||||
const body = m[3] || '';
|
||||
let ts = '', lvl = '', body = '';
|
||||
if (m) {
|
||||
ts = formatTs(m[1]);
|
||||
lvl = m[2];
|
||||
body = m[3] || '';
|
||||
} else {
|
||||
m = line.match(RE_PLAIN_LEVEL_LINE);
|
||||
if (!m) return null;
|
||||
lvl = m[1];
|
||||
body = m[2] || '';
|
||||
}
|
||||
const kvStart = body.search(/(?:^|\s)(?=[A-Za-z_][\w.-]*=)/);
|
||||
return {
|
||||
ts: formatTs(m[1]),
|
||||
lvl: m[2],
|
||||
ts: ts,
|
||||
lvl: lvl,
|
||||
msg: kvStart === -1 ? body : body.slice(0, kvStart).trimEnd(),
|
||||
kv: kvStart === -1 ? '' : body.slice(kvStart).trim()
|
||||
};
|
||||
@@ -135,11 +145,10 @@ function buildLine(ln) {
|
||||
if (!parsed) {
|
||||
return E('div', { 'class': 'dd-line ' + cls }, ln);
|
||||
}
|
||||
const parts = [
|
||||
E('span', { 'class': 'dd-ts' }, parsed.ts),
|
||||
E('span', { 'class': 'dd-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)),
|
||||
E('span', { 'class': 'dd-msg' }, parsed.msg)
|
||||
];
|
||||
const parts = [];
|
||||
if (parsed.ts) parts.push(E('span', { 'class': 'dd-ts' }, parsed.ts));
|
||||
parts.push(E('span', { 'class': 'dd-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)));
|
||||
parts.push(E('span', { 'class': 'dd-msg' }, parsed.msg));
|
||||
if (parsed.kv) parts.push(E('span', { 'class': 'dd-kv' }, parsed.kv));
|
||||
return E('div', { 'class': 'dd-line ' + cls }, parts);
|
||||
}
|
||||
@@ -158,7 +167,8 @@ return view.extend({
|
||||
paused: false,
|
||||
autoScroll: true,
|
||||
filter: '',
|
||||
userScrolled: false
|
||||
userScrolled: false,
|
||||
reading: false
|
||||
};
|
||||
|
||||
const pane = E('div', { 'class': 'dd-log-pane', 'id': 'dd-log-pane' }, [
|
||||
@@ -173,19 +183,29 @@ return view.extend({
|
||||
|
||||
const meta = E('span', { 'class': 'dd-log-meta' }, '');
|
||||
|
||||
const cbAuto = E('input', { 'type': 'checkbox', 'checked': 'checked' });
|
||||
cbAuto.addEventListener('change', function() {
|
||||
state.autoScroll = cbAuto.checked;
|
||||
const btnAuto = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-auto' });
|
||||
const btnPause = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-pause' });
|
||||
const syncControls = function() {
|
||||
btnAuto.textContent = state.autoScroll ? '✓ ' + _('Auto-scroll: On') : '○ ' + _('Auto-scroll: Off');
|
||||
btnAuto.setAttribute('aria-pressed', String(state.autoScroll));
|
||||
btnPause.textContent = state.paused ? '▶ ' + _('Resume') : 'Ⅱ ' + _('Pause');
|
||||
btnPause.setAttribute('aria-pressed', String(state.paused));
|
||||
meta.textContent = state.paused ? _('Paused') : '';
|
||||
};
|
||||
btnAuto.addEventListener('click', function() {
|
||||
state.autoScroll = !state.autoScroll;
|
||||
if (state.autoScroll) {
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
state.userScrolled = false;
|
||||
}
|
||||
syncControls();
|
||||
});
|
||||
|
||||
const cbPause = E('input', { 'type': 'checkbox' });
|
||||
cbPause.addEventListener('change', function() {
|
||||
state.paused = cbPause.checked;
|
||||
btnPause.addEventListener('click', function() {
|
||||
state.paused = !state.paused;
|
||||
syncControls();
|
||||
if (!state.paused) tick();
|
||||
});
|
||||
syncControls();
|
||||
|
||||
const selFilter = E('select', { 'class': 'dd-log-btn' }, [
|
||||
E('option', { 'value': '' }, _('All')),
|
||||
@@ -287,9 +307,11 @@ return view.extend({
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (state.paused) return Promise.resolve();
|
||||
if (state.paused || state.reading) return Promise.resolve();
|
||||
state.reading = true;
|
||||
|
||||
return fs.stat(LOG_PATH).then(function(st) {
|
||||
if (state.paused) return;
|
||||
const size = st.size || 0;
|
||||
if (size === state.lastSize) {
|
||||
meta.textContent = '%d bytes · live'.format(size);
|
||||
@@ -299,6 +321,7 @@ return view.extend({
|
||||
// File rotated/truncated → full reload
|
||||
const rotated = size < state.lastSize;
|
||||
return fs.read_direct(LOG_PATH, 'text').then(function(content) {
|
||||
if (state.paused) return;
|
||||
content = content || '';
|
||||
let delta;
|
||||
if (rotated || state.lastContent === '') {
|
||||
@@ -323,6 +346,7 @@ return view.extend({
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
});
|
||||
}).catch(function(e) {
|
||||
if (state.paused) return;
|
||||
const msg = String(e);
|
||||
if (msg.indexOf('NotFoundError') !== -1 || msg.indexOf('No such') !== -1)
|
||||
renderEmpty(_('Log file does not exist yet.'));
|
||||
@@ -331,15 +355,15 @@ return view.extend({
|
||||
state.lastSize = -1;
|
||||
state.lastContent = '';
|
||||
meta.textContent = '';
|
||||
});
|
||||
}).finally(function() { state.reading = false; });
|
||||
}
|
||||
|
||||
poll.add(tick);
|
||||
tick();
|
||||
|
||||
const toolbarItems = [
|
||||
E('label', {}, [ cbAuto, _('Auto-scroll') ]),
|
||||
E('label', {}, [ cbPause, _('Pause') ]),
|
||||
btnAuto,
|
||||
btnPause,
|
||||
selFilter,
|
||||
btnClear,
|
||||
btnDownload,
|
||||
@@ -350,7 +374,7 @@ return view.extend({
|
||||
const toolbar = E('div', { 'class': 'dd-log-toolbar' }, toolbarItems);
|
||||
|
||||
return E('div', { 'class': 'dd-log-wrap' }, [
|
||||
E('style', {}, CSS),
|
||||
E('style', {}, CSS + styles.CSS),
|
||||
E('div', { 'class': 'dd-log-card' }, [
|
||||
E('h4', { 'class': 'dd-log-card-title' }, _('%s Realtime Log').format(ctx.name)),
|
||||
toolbar,
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
'require baseclass';
|
||||
|
||||
const CSS = [
|
||||
'.dd-log-toolbar .dd-log-toggle[aria-pressed="true"]{background:rgba(74,160,101,.16);border-color:#4aa065;box-shadow:inset 0 0 0 1px rgba(74,160,101,.2)}',
|
||||
'.dd-log-toolbar .dd-log-toggle:focus-visible{outline:2px solid #4aa065;outline-offset:2px}',
|
||||
|
||||
'.dd-wrap{padding:4px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
|
||||
'.dd-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:9px 14px;margin-bottom:7px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
|
||||
/* padding:0 neutralizes Argon's h4{padding:.75rem 1.25rem}, which otherwise
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
'require view.daede.daed-session as daedSession';
|
||||
|
||||
const DATA_PATHS = {
|
||||
geoip: '/usr/share/v2ray/geoip.dat',
|
||||
@@ -53,6 +54,9 @@ const CSS = [
|
||||
'.dd-up-btn:hover{background:rgba(128,128,128,.12)}',
|
||||
'.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}',
|
||||
'.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}',
|
||||
'.dd-backup-buttons{display:flex;align-items:center;gap:8px}',
|
||||
'.dd-backup-reset{border-color:rgba(217,109,109,.55);color:#d96d6d}',
|
||||
'@media(max-width:640px){.dd-backup-row{grid-template-columns:24px minmax(0,1fr)}.dd-backup-row .dd-up-meta{grid-column:2;white-space:normal}.dd-backup-row>span:nth-child(4){display:none}.dd-backup-buttons{grid-column:2;flex-wrap:wrap}}',
|
||||
'.dd-geo-row{display:grid;grid-template-columns:96px 1fr;gap:10px;align-items:center;font-size:12px;padding:6px 0}',
|
||||
'.dd-geo-row label{opacity:.75;font-weight:600}',
|
||||
'.dd-geo-row input[type=text],.dd-geo-row select{font-size:12px;padding:4px 8px;border:1px solid rgba(128,128,128,.35);border-radius:5px;background:transparent;color:inherit;width:100%}',
|
||||
@@ -202,65 +206,126 @@ return view.extend({
|
||||
return runJob('update-pkg.sh', pkg, btn, '/tmp/luci-app-daede.pkg-' + pkg + '.log');
|
||||
};
|
||||
|
||||
// === Config backup (export / import the whole daede config) ===
|
||||
const exportBtn = E('button', { 'class': 'dd-up-btn' }, _('Export'));
|
||||
const importBtn = E('button', { 'class': 'dd-up-btn' }, _('Import'));
|
||||
// Configuration operations share a backend lock and one UI busy state.
|
||||
const backupScript = '/usr/share/luci-app-daede/config-backup.sh';
|
||||
const exportBtn = E('button', { 'class': 'dd-up-btn', 'type': 'button' }, _('Export'));
|
||||
const importBtn = E('button', { 'class': 'dd-up-btn', 'type': 'button' }, _('Import'));
|
||||
const resetBtn = E('button', { 'class': 'dd-up-btn dd-backup-reset', 'type': 'button' }, _('Restore Defaults'));
|
||||
const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' });
|
||||
|
||||
exportBtn.addEventListener('click', function() {
|
||||
const orig = exportBtn.textContent;
|
||||
exportBtn.disabled = true; exportBtn.textContent = '...';
|
||||
fs.exec('/usr/share/luci-app-daede/config-backup.sh', ['export']).then(function(res) {
|
||||
if (res.code !== 0 || !res.stdout) {
|
||||
logPane.textContent = String(res.stderr || res.stdout || 'export failed').trim();
|
||||
let backupBusy = false;
|
||||
const showBackupError = function(e) {
|
||||
ui.addNotification(null, E('p', {}, e.message || String(e)), 'error');
|
||||
};
|
||||
const configOperation = function(button, operation) {
|
||||
if (backupBusy) return Promise.resolve();
|
||||
backupBusy = true;
|
||||
const label = button.textContent;
|
||||
[exportBtn, importBtn, resetBtn].forEach(function(b) { b.disabled = true; });
|
||||
button.textContent = _('Working…');
|
||||
return uci.changes().then(function(changes) {
|
||||
if (['dae', 'daed', 'daede'].some(function(name) { return changes && changes[name] && changes[name].length; }))
|
||||
throw new Error(_('Apply or discard pending daede changes first.'));
|
||||
return operation();
|
||||
}).catch(showBackupError).finally(function() {
|
||||
backupBusy = false;
|
||||
[exportBtn, importBtn, resetBtn].forEach(function(b) { b.disabled = false; });
|
||||
button.textContent = label;
|
||||
fileInput.value = '';
|
||||
});
|
||||
};
|
||||
const confirmBackup = function(message) {
|
||||
return new Promise(function(resolve) {
|
||||
const answer = function(value) { ui.hideModal(); resolve(value); };
|
||||
ui.showModal(_('Confirm configuration operation'), [
|
||||
E('p', {}, message),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': function() { answer(false); } }, _('Cancel')),
|
||||
' ',
|
||||
E('button', { 'class': 'cbi-button cbi-button-negative', 'click': function() { answer(true); } }, _('Continue'))
|
||||
])
|
||||
]);
|
||||
});
|
||||
};
|
||||
const execBackup = function(args) {
|
||||
return fs.exec(backupScript, args).then(function(res) {
|
||||
if (!res || res.code !== 0)
|
||||
throw new Error(String(res && (res.stderr || res.stdout) || _('Configuration operation failed.')).trim());
|
||||
return res;
|
||||
});
|
||||
};
|
||||
const waitBackup = function() {
|
||||
let tries = 0;
|
||||
const check = function() {
|
||||
return fs.read_direct('/tmp/luci-app-daede.backup.log', 'text').then(function(content) {
|
||||
logPane.textContent = content;
|
||||
logPane.classList.add('show');
|
||||
return;
|
||||
}
|
||||
let bin;
|
||||
try {
|
||||
bin = atob(res.stdout.trim());
|
||||
} catch (e) {
|
||||
logPane.textContent = _('Export failed: invalid base64 data from server');
|
||||
logPane.classList.add('show');
|
||||
return;
|
||||
}
|
||||
if (!bin || bin.length === 0) {
|
||||
logPane.textContent = _('Export failed: empty archive');
|
||||
logPane.classList.add('show');
|
||||
return;
|
||||
}
|
||||
const arr = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
||||
const url = URL.createObjectURL(new Blob([arr], { 'type': 'application/gzip' }));
|
||||
const a = E('a', { 'href': url, 'download': 'daede-config-' + stamp() + '.tar.gz' });
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
|
||||
}).catch(function(e) {
|
||||
logPane.textContent = _('Export failed') + ': ' + (e ? (e.message || String(e)) : _('script not found'));
|
||||
logPane.classList.add('show');
|
||||
}).finally(function() {
|
||||
exportBtn.disabled = false; exportBtn.textContent = orig;
|
||||
if (/^✗/m.test(content)) throw new Error(_('Configuration operation failed. See the log below.'));
|
||||
if (/^✓/m.test(content)) return;
|
||||
if (++tries > 90) throw new Error(_('Operation is still running. Check the log before retrying.'));
|
||||
return new Promise(function(resolve) { setTimeout(resolve, 2000); }).then(check);
|
||||
});
|
||||
};
|
||||
return check();
|
||||
};
|
||||
const reloadConfig = function(message) {
|
||||
daedSession.clear(window.localStorage);
|
||||
['dae', 'daed', 'daede'].forEach(function(name) { uci.unload(name); });
|
||||
ui.showModal(_('Configuration restored'), [
|
||||
E('p', {}, message),
|
||||
E('div', { 'class': 'right' }, E('button', {
|
||||
'class': 'cbi-button cbi-button-positive', 'click': function() { window.location.reload(); }
|
||||
}, _('Reload Page')))
|
||||
]);
|
||||
};
|
||||
exportBtn.addEventListener('click', async function() {
|
||||
if (!await confirmBackup(_('Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?'))) return;
|
||||
configOperation(exportBtn, function() {
|
||||
return execBackup(['export']).then(function(res) {
|
||||
const bin = atob((res.stdout || '').trim());
|
||||
if (!bin) throw new Error(_('Export failed: empty archive'));
|
||||
const bytes = Uint8Array.from(bin, function(c) { return c.charCodeAt(0); });
|
||||
const url = URL.createObjectURL(new Blob([bytes], { 'type': 'application/gzip' }));
|
||||
const a = E('a', { 'href': url, 'download': 'daede-config-' + stamp() + '.tar.gz' });
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
importBtn.addEventListener('click', function() { fileInput.click(); });
|
||||
fileInput.addEventListener('change', function(ev) {
|
||||
fileInput.addEventListener('change', async function(ev) {
|
||||
const file = ev.target.files && ev.target.files[0];
|
||||
if (!file) return;
|
||||
if (!confirm(_('Import overwrites the current daede config and restarts the backend. Continue?'))) {
|
||||
if (file.size > 184320) {
|
||||
showBackupError(new Error(_('Backup exceeds the 180 KiB limit. Use System Backup for larger files.')));
|
||||
fileInput.value = ''; return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const b64 = String(e.target.result).split(',')[1] || '';
|
||||
fs.write('/tmp/daede-import.b64', b64).then(function() {
|
||||
return runJob('config-backup.sh', 'import', importBtn, '/tmp/luci-app-daede.backup.log');
|
||||
}).catch(function(e) {
|
||||
logPane.textContent = _('Import failed') + ': ' + (e ? (e.message || String(e)) : _('unknown error'));
|
||||
logPane.classList.add('show');
|
||||
}).finally(function() { fileInput.value = ''; });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
if (!await confirmBackup(_('Import replaces daede settings, nodes, subscriptions and dashboard data. Services stop first; the saved active backend starts only if enabled in the backup. Continue?'))) {
|
||||
fileInput.value = ''; return;
|
||||
}
|
||||
configOperation(importBtn, function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = function() { reject(new Error(_('Unable to read backup file.'))); };
|
||||
reader.onload = function() { resolve(String(reader.result).split(',')[1] || ''); };
|
||||
reader.readAsDataURL(file);
|
||||
}).then(function(b64) {
|
||||
// Per-tab uploads cannot overwrite each other before the backend locks.
|
||||
const token = Array.from(window.crypto.getRandomValues(new Uint8Array(16)), function(n) { return n.toString(16).padStart(2, '0'); }).join('');
|
||||
const upload = '/tmp/daede-import.' + token + '.b64';
|
||||
return fs.write(upload, b64).then(function() { return execBackup(['import', upload]); })
|
||||
.finally(function() { return fs.remove(upload).catch(function() {}); });
|
||||
}).then(waitBackup).then(function() {
|
||||
reloadConfig(_('Backup restored. The active backend follows the saved enabled setting.'));
|
||||
});
|
||||
});
|
||||
});
|
||||
resetBtn.addEventListener('click', async function() {
|
||||
if (!await confirmBackup(_('Restore ALL daede defaults? This clears dae/daed settings, nodes, subscriptions and daed dashboard accounts/data. Export a backup first if needed. Both backends will remain disabled. Installed programs and GeoData are kept.'))) return;
|
||||
configOperation(resetBtn, function() {
|
||||
return execBackup(['reset']).then(waitBackup).then(function() {
|
||||
reloadConfig(_('All defaults restored. Both backends are disabled. Configure your nodes and routing before starting manually.'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const refresh = function() {
|
||||
@@ -508,12 +573,15 @@ return view.extend({
|
||||
]),
|
||||
E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Config Backup')),
|
||||
E('div', { 'class': 'dd-up-row' }, [
|
||||
E('span', { 'class': 'dd-up-icon dd-up-new' }, '⤓'),
|
||||
E('div', { 'class': 'dd-up-row dd-backup-row' }, [
|
||||
E('span', { 'class': 'dd-up-icon dd-up-new' }, '⇩'),
|
||||
E('span', { 'class': 'dd-up-name' }, _('dae + daed')),
|
||||
E('span', { 'class': 'dd-up-meta' }, _('Back up / restore the whole daede config (kernels excluded)')),
|
||||
exportBtn,
|
||||
importBtn
|
||||
E('span', {
|
||||
'class': 'dd-up-meta',
|
||||
'title': _('Backups contain account and subscription credentials. Store them securely.')
|
||||
}, _('Back up / restore the whole daede config (kernels excluded)')),
|
||||
E('span', {}, ''),
|
||||
E('div', { 'class': 'dd-backup-buttons' }, [exportBtn, importBtn, resetBtn])
|
||||
]),
|
||||
fileInput
|
||||
]),
|
||||
|
||||
@@ -1025,6 +1025,12 @@ msgstr "导出"
|
||||
msgid "Import"
|
||||
msgstr "导入"
|
||||
|
||||
msgid "Restore Defaults"
|
||||
msgstr "恢复默认"
|
||||
|
||||
msgid "Backups contain account and subscription credentials. Store them securely."
|
||||
msgstr "备份中包含账号和订阅凭据,请妥善保管。"
|
||||
|
||||
msgid "Back up / restore the whole daede config (kernels excluded)"
|
||||
msgstr "备份 / 还原整个 daede 配置(不含内核)"
|
||||
|
||||
@@ -1086,3 +1092,75 @@ msgid ""
|
||||
"Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set "
|
||||
"it, e.g. eth0.2."
|
||||
msgstr "上行 WAN 接口。auto 按默认路由自动检测;老式 swconfig 交换机或多 WAN 时手动指定,例如 eth0.2。"
|
||||
|
||||
msgid "Export Backup"
|
||||
msgstr "导出备份"
|
||||
|
||||
msgid "Import Backup"
|
||||
msgstr "导入还原"
|
||||
|
||||
msgid "Restore All Defaults"
|
||||
msgstr "恢复全部默认配置"
|
||||
|
||||
msgid "Working…"
|
||||
msgstr "正在处理…"
|
||||
|
||||
msgid "Apply or discard pending daede changes first."
|
||||
msgstr "请先应用或放弃尚未保存的 daede 修改。"
|
||||
|
||||
msgid "Configuration operation failed."
|
||||
msgstr "配置操作失败。"
|
||||
|
||||
msgid "Configuration operation failed. See the log below."
|
||||
msgstr "配置操作失败,请查看下方日志。"
|
||||
|
||||
msgid "Operation is still running. Check the log before retrying."
|
||||
msgstr "操作仍在进行,请检查日志后再重试。"
|
||||
|
||||
msgid "Configuration restored"
|
||||
msgstr "配置已恢复"
|
||||
|
||||
msgid "Reload Page"
|
||||
msgstr "刷新页面"
|
||||
|
||||
msgid "Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?"
|
||||
msgstr "导出时会短暂停止正在运行的 dae/daed,以保证数据库备份完整,完成后恢复运行。是否继续?"
|
||||
|
||||
msgid "Backup exceeds the 180 KiB limit. Use System Backup for larger files."
|
||||
msgstr "备份文件超过 180 KiB 限制,请使用系统备份功能处理较大的文件。"
|
||||
|
||||
msgid "Import replaces daede settings, nodes, subscriptions and dashboard data. Services stop first; the saved active backend starts only if enabled in the backup. Continue?"
|
||||
msgstr "导入将覆盖 daede 设置、节点、订阅和面板数据。服务会先停止,仅在备份中为启用状态时启动对应后端。是否继续?"
|
||||
|
||||
msgid "Unable to read backup file."
|
||||
msgstr "无法读取备份文件。"
|
||||
|
||||
msgid "Backup restored. The active backend follows the saved enabled setting."
|
||||
msgstr "备份已还原,当前后端按备份中的启用状态运行。"
|
||||
|
||||
msgid "Restore ALL daede defaults? This clears dae/daed settings, nodes, subscriptions and daed dashboard accounts/data. Export a backup first if needed. Both backends will remain disabled. Installed programs and GeoData are kept."
|
||||
msgstr "确定恢复 daede 全部默认配置?此操作会清空 dae/daed 设置、节点、订阅以及 daed 面板账号和数据。需要保留时请先导出备份。完成后两个后端均保持关闭,已安装程序和 GeoData 数据文件保留。"
|
||||
|
||||
msgid "All defaults restored. Both backends are disabled. Configure your nodes and routing before starting manually."
|
||||
msgstr "已恢复全部默认配置,两个后端均已关闭。请重新配置节点和路由后手动开启。"
|
||||
|
||||
msgid "Back up settings, nodes, subscriptions and dashboard data. Backups contain credentials; store them securely. Kernels and GeoData are excluded."
|
||||
msgstr "备份包括设置、节点、订阅和面板数据,不包括内核和 GeoData。备份中含有账号和订阅凭据,请妥善保管。"
|
||||
|
||||
msgid "Auto-scroll: On"
|
||||
msgstr "自动滚动:开启"
|
||||
|
||||
msgid "Auto-scroll: Off"
|
||||
msgstr "自动滚动:关闭"
|
||||
|
||||
msgid "Resume"
|
||||
msgstr "继续"
|
||||
|
||||
msgid "Paused"
|
||||
msgstr "已暂停"
|
||||
|
||||
msgid "Confirm configuration operation"
|
||||
msgstr "确认配置操作"
|
||||
|
||||
msgid "Continue"
|
||||
msgstr "继续"
|
||||
@@ -1025,6 +1025,12 @@ msgstr "导出"
|
||||
msgid "Import"
|
||||
msgstr "导入"
|
||||
|
||||
msgid "Restore Defaults"
|
||||
msgstr "恢复默认"
|
||||
|
||||
msgid "Backups contain account and subscription credentials. Store them securely."
|
||||
msgstr "备份中包含账号和订阅凭据,请妥善保管。"
|
||||
|
||||
msgid "Back up / restore the whole daede config (kernels excluded)"
|
||||
msgstr "备份 / 还原整个 daede 配置(不含内核)"
|
||||
|
||||
@@ -1086,3 +1092,75 @@ msgid ""
|
||||
"Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set "
|
||||
"it, e.g. eth0.2."
|
||||
msgstr "上行 WAN 接口。auto 按默认路由自动检测;老式 swconfig 交换机或多 WAN 时手动指定,例如 eth0.2。"
|
||||
|
||||
msgid "Export Backup"
|
||||
msgstr "导出备份"
|
||||
|
||||
msgid "Import Backup"
|
||||
msgstr "导入还原"
|
||||
|
||||
msgid "Restore All Defaults"
|
||||
msgstr "恢复全部默认配置"
|
||||
|
||||
msgid "Working…"
|
||||
msgstr "正在处理…"
|
||||
|
||||
msgid "Apply or discard pending daede changes first."
|
||||
msgstr "请先应用或放弃尚未保存的 daede 修改。"
|
||||
|
||||
msgid "Configuration operation failed."
|
||||
msgstr "配置操作失败。"
|
||||
|
||||
msgid "Configuration operation failed. See the log below."
|
||||
msgstr "配置操作失败,请查看下方日志。"
|
||||
|
||||
msgid "Operation is still running. Check the log before retrying."
|
||||
msgstr "操作仍在进行,请检查日志后再重试。"
|
||||
|
||||
msgid "Configuration restored"
|
||||
msgstr "配置已恢复"
|
||||
|
||||
msgid "Reload Page"
|
||||
msgstr "刷新页面"
|
||||
|
||||
msgid "Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?"
|
||||
msgstr "导出时会短暂停止正在运行的 dae/daed,以保证数据库备份完整,完成后恢复运行。是否继续?"
|
||||
|
||||
msgid "Backup exceeds the 180 KiB limit. Use System Backup for larger files."
|
||||
msgstr "备份文件超过 180 KiB 限制,请使用系统备份功能处理较大的文件。"
|
||||
|
||||
msgid "Import replaces daede settings, nodes, subscriptions and dashboard data. Services stop first; the saved active backend starts only if enabled in the backup. Continue?"
|
||||
msgstr "导入将覆盖 daede 设置、节点、订阅和面板数据。服务会先停止,仅在备份中为启用状态时启动对应后端。是否继续?"
|
||||
|
||||
msgid "Unable to read backup file."
|
||||
msgstr "无法读取备份文件。"
|
||||
|
||||
msgid "Backup restored. The active backend follows the saved enabled setting."
|
||||
msgstr "备份已还原,当前后端按备份中的启用状态运行。"
|
||||
|
||||
msgid "Restore ALL daede defaults? This clears dae/daed settings, nodes, subscriptions and daed dashboard accounts/data. Export a backup first if needed. Both backends will remain disabled. Installed programs and GeoData are kept."
|
||||
msgstr "确定恢复 daede 全部默认配置?此操作会清空 dae/daed 设置、节点、订阅以及 daed 面板账号和数据。需要保留时请先导出备份。完成后两个后端均保持关闭,已安装程序和 GeoData 数据文件保留。"
|
||||
|
||||
msgid "All defaults restored. Both backends are disabled. Configure your nodes and routing before starting manually."
|
||||
msgstr "已恢复全部默认配置,两个后端均已关闭。请重新配置节点和路由后手动开启。"
|
||||
|
||||
msgid "Back up settings, nodes, subscriptions and dashboard data. Backups contain credentials; store them securely. Kernels and GeoData are excluded."
|
||||
msgstr "备份包括设置、节点、订阅和面板数据,不包括内核和 GeoData。备份中含有账号和订阅凭据,请妥善保管。"
|
||||
|
||||
msgid "Auto-scroll: On"
|
||||
msgstr "自动滚动:开启"
|
||||
|
||||
msgid "Auto-scroll: Off"
|
||||
msgstr "自动滚动:关闭"
|
||||
|
||||
msgid "Resume"
|
||||
msgstr "继续"
|
||||
|
||||
msgid "Paused"
|
||||
msgstr "已暂停"
|
||||
|
||||
msgid "Confirm configuration operation"
|
||||
msgstr "确认配置操作"
|
||||
|
||||
msgid "Continue"
|
||||
msgstr "继续"
|
||||
@@ -79,27 +79,7 @@ fi
|
||||
# Idempotent: only add what's missing. Lives in the `dae` UCI package,
|
||||
# consumed by gen-dae-config.sh.
|
||||
if [ -f /etc/config/dae ]; then
|
||||
if ! uci -q get dae.config.lan_interface >/dev/null 2>&1; then
|
||||
uci -q set dae.config.lan_interface='br-lan'
|
||||
fi
|
||||
if ! uci -q show dae | grep -q "=group$"; then
|
||||
g="$(uci add dae group)"
|
||||
uci -q set "dae.$g.name=proxy"
|
||||
uci -q set "dae.$g.policy=min_moving_avg"
|
||||
fi
|
||||
if ! uci -q get dae.routing >/dev/null 2>&1; then
|
||||
uci -q set dae.routing=routing
|
||||
uci -q set dae.routing.private_direct=1
|
||||
uci -q set dae.routing.cn_direct=1
|
||||
uci -q set dae.routing.block_ads=0
|
||||
uci -q set dae.routing.fallback=proxy
|
||||
fi
|
||||
if ! uci -q get dae.dns >/dev/null 2>&1; then
|
||||
uci -q set dae.dns=dns
|
||||
uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53'
|
||||
uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53'
|
||||
uci -q set dae.dns.response_ttl='0'
|
||||
fi
|
||||
/usr/share/luci-app-daede/config-defaults.sh
|
||||
# Migrate legacy group filters to the unified source / name_filter fields,
|
||||
# then drop the old keys so the form shows real, editable values (the old
|
||||
# fields used to re-populate the new ones and looked un-deletable).
|
||||
|
||||
Regular → Executable
+241
-62
@@ -1,71 +1,250 @@
|
||||
#!/bin/sh
|
||||
# config-backup.sh export|import — back up/restore the whole daede config
|
||||
# (dae + daed + active backend). export: prints base64(tar.gz) to stdout.
|
||||
# import: decodes /tmp/daede-import.b64, validates, restores, restarts backend.
|
||||
# Whole-plugin backup/import/reset. Only fixed configuration paths are managed.
|
||||
set -eu
|
||||
umask 077
|
||||
|
||||
ACTION="${1:-}"
|
||||
LOG="/tmp/luci-app-daede.backup.log"
|
||||
IMPORT_B64="/tmp/daede-import.b64"
|
||||
MAX_TAR=184320 # 180 KiB tar.gz -> ~240 KiB base64, under the ubus limit
|
||||
|
||||
# relative paths (no leading /) so tar entries match the extraction whitelist
|
||||
WHITELIST="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db"
|
||||
UPLOAD="${2:-/tmp/daede-import.b64}"
|
||||
LOG=/tmp/luci-app-daede.backup.log
|
||||
LOCK=/tmp/luci-app-daede.config.lock
|
||||
SHARE=/usr/share/luci-app-daede
|
||||
MAX_TAR=184320 # Base64 must fit inside the ubus reply (~240 KiB).
|
||||
MAX_RAW=16777216
|
||||
FILES="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db etc/daed/wing.db-wal etc/daed/wing.db-shm"
|
||||
WORK=""
|
||||
SNAPSHOT=0
|
||||
MUTATING=0
|
||||
KEEP=0
|
||||
RESTART=""
|
||||
|
||||
fail() { echo "$*" >&2; exit 1; }
|
||||
b64enc() { ucode -e 'let f=require("fs"); print(b64enc(f.open(ARGV[0],"r").read("all")));' -- "$1"; }
|
||||
b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); o.write(r); o.close();' -- "$1" "$2"; }
|
||||
b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); if(!o || o.write(r)!=length(r))exit(1); o.close();' -- "$1" "$2"; }
|
||||
# Do not merge pending CLI UCI deltas into snapshots/defaults.
|
||||
config() { /sbin/uci -c /etc/config -t "$WORK/uci" "$@"; }
|
||||
|
||||
case "$ACTION" in
|
||||
export)
|
||||
exist=""
|
||||
for p in $WHITELIST; do [ -e "/$p" ] && exist="$exist $p"; done
|
||||
[ -n "$exist" ] || { echo "no config to back up" >&2; exit 1; }
|
||||
tmp="$(mktemp)"
|
||||
( cd / && tar -czf "$tmp" $exist ) 2>/dev/null || { rm -f "$tmp"; echo "tar failed" >&2; exit 1; }
|
||||
if [ "$(wc -c < "$tmp")" -gt "$MAX_TAR" ]; then
|
||||
rm -f "$tmp"; echo "config too large; use System Backup instead" >&2; exit 2
|
||||
fi
|
||||
b64enc "$tmp"
|
||||
rm -f "$tmp"
|
||||
;;
|
||||
|
||||
import)
|
||||
[ -f "$IMPORT_B64" ] || { echo "no upload found" >&2; exit 1; }
|
||||
(
|
||||
exec >"$LOG" 2>&1
|
||||
echo "$(date '+%F %T') begin import"
|
||||
rc=0
|
||||
tmp="$(mktemp)"
|
||||
if ! b64dec "$IMPORT_B64" "$tmp"; then
|
||||
echo "decode failed"; rc=1
|
||||
elif ! gzip -t "$tmp" 2>/dev/null; then
|
||||
echo "not a valid backup archive"; rc=1
|
||||
else
|
||||
# reject any entry outside the whitelist (path-traversal guard)
|
||||
bad="$(tar -tzf "$tmp" 2>/dev/null | grep -vxE 'etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config\.dae|etc/daed/wing\.db' | head -1)"
|
||||
if [ -n "$bad" ]; then
|
||||
echo "rejected: unexpected entry '$bad'"; rc=1
|
||||
else
|
||||
ab="$(uci -q get daede.config.active_backend || echo dae)"
|
||||
[ -x "/etc/init.d/$ab" ] && /etc/init.d/"$ab" stop 2>/dev/null || true
|
||||
if ( cd / && tar -xzf "$tmp" ); then
|
||||
echo "restored config"
|
||||
else
|
||||
echo "extract failed"; rc=1
|
||||
fi
|
||||
ab="$(uci -q get daede.config.active_backend || echo "$ab")"
|
||||
[ -x "/etc/init.d/$ab" ] && { /etc/init.d/"$ab" enabled || /etc/init.d/"$ab" enable; /etc/init.d/"$ab" restart 2>/dev/null || /etc/init.d/"$ab" start 2>/dev/null; } || true
|
||||
fi
|
||||
paths() {
|
||||
printf '%s\n' $FILES
|
||||
for sub in "$1"/etc/dae/subscriptions/*.sub; do
|
||||
[ -e "$sub" ] || [ -L "$sub" ] || continue
|
||||
name="${sub##*/}"
|
||||
case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'unexpected subscription filename' ;; esac
|
||||
printf 'etc/dae/subscriptions/%s\n' "$name"
|
||||
done
|
||||
}
|
||||
check_paths() {
|
||||
for dir in /etc/config /etc/dae /etc/daed /etc/dae/subscriptions; do
|
||||
[ ! -L "$dir" ] || fail "refusing symlink: $dir"
|
||||
done
|
||||
paths / > "$WORK/paths"
|
||||
while IFS= read -r p; do
|
||||
[ ! -L "/$p" ] || fail "refusing symlink: /$p"
|
||||
[ ! -e "/$p" ] || [ -f "/$p" ] || fail "not a regular file: /$p"
|
||||
done < "$WORK/paths"
|
||||
}
|
||||
snapshot() {
|
||||
mkdir -p "$WORK/before"
|
||||
while IFS= read -r p; do
|
||||
[ -f "/$p" ] || continue
|
||||
mkdir -p "$WORK/before/${p%/*}"
|
||||
cp -p "/$p" "$WORK/before/$p"
|
||||
done < "$WORK/paths"
|
||||
SNAPSHOT=1
|
||||
}
|
||||
stop_backends() {
|
||||
for svc in dae daed; do
|
||||
[ -x "/etc/init.d/$svc" ] || continue
|
||||
if /etc/init.d/"$svc" running >/dev/null 2>&1; then RESTART="$RESTART $svc"; fi
|
||||
/etc/init.d/"$svc" stop >/dev/null 2>&1 || fail "failed to stop $svc; configuration unchanged"
|
||||
done
|
||||
# procd termination can be asynchronous. Never copy a live SQLite database.
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if ! pidof dae daed daed-guard >/dev/null 2>&1; then return; fi
|
||||
sleep 1
|
||||
done
|
||||
fail 'backend still running; configuration unchanged'
|
||||
}
|
||||
disable_backends() {
|
||||
for svc in dae daed; do
|
||||
if [ -f "/etc/config/$svc" ]; then
|
||||
config -q set "$svc.config=$svc" || return 1
|
||||
config -q set "$svc.config.enabled=0" || return 1
|
||||
config -q commit "$svc" || return 1
|
||||
fi
|
||||
rm -f "$tmp" "$IMPORT_B64"
|
||||
if [ "$rc" = 0 ]; then echo "result: config restored, backend restarted"; else echo "result: import failed"; fi
|
||||
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
echo "started in background, see $LOG"
|
||||
;;
|
||||
if [ -x "/etc/init.d/$svc" ]; then
|
||||
/etc/init.d/"$svc" disable >/dev/null 2>&1 || return 1
|
||||
/etc/init.d/"$svc" stop >/dev/null 2>&1 || return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
restore_snapshot() {
|
||||
# Remove files created by the failed operation as well as original files.
|
||||
paths / > "$WORK/current" || return 1
|
||||
while IFS= read -r p; do rm -f "/$p" || return 1; done < "$WORK/current"
|
||||
while IFS= read -r p; do
|
||||
[ -f "$WORK/before/$p" ] || continue
|
||||
mkdir -p "/${p%/*}" || return 1
|
||||
cp -p "$WORK/before/$p" "/$p" || return 1
|
||||
done < "$WORK/paths"
|
||||
}
|
||||
sync_cron() {
|
||||
geo=disable; sub=disable
|
||||
[ "$(config -q get daede.config.geo_auto || :)" != 1 ] || geo=enable
|
||||
[ "$(config -q get daed.config.subscribe_auto_update || :)" != 1 ] || sub=enable
|
||||
"$SHARE/geo-cron.sh" "$geo" >/dev/null 2>&1
|
||||
"$SHARE/daed-sub-cron.sh" "$sub" >/dev/null 2>&1
|
||||
}
|
||||
finish() {
|
||||
rc=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
if [ "$rc" -ne 0 ] && [ "$MUTATING" = 1 ]; then
|
||||
disable_backends || true
|
||||
if [ "$SNAPSHOT" = 1 ] && ! restore_snapshot; then
|
||||
KEEP=1
|
||||
echo "rollback failed; root-only recovery files retained at $WORK" >&2
|
||||
else
|
||||
echo 'previous configuration restored; backends remain disabled' >&2
|
||||
fi
|
||||
disable_backends || { KEEP=1; echo 'failed to disable backend; check service state' >&2; }
|
||||
sync_cron || true
|
||||
fi
|
||||
if [ "$ACTION" = export ]; then
|
||||
for svc in $RESTART; do
|
||||
/etc/init.d/"$svc" start >/dev/null 2>&1 || { rc=1; echo "failed to resume $svc" >&2; }
|
||||
done
|
||||
fi
|
||||
if [ "$ACTION" != export ]; then
|
||||
if [ "$rc" = 0 ]; then echo '✓ 完成'; else echo '✗ 失败'; fi
|
||||
fi
|
||||
[ "$KEEP" = 1 ] || rm -rf "$WORK"
|
||||
rmdir "$LOCK"
|
||||
exit "$rc"
|
||||
}
|
||||
validate_archive() {
|
||||
[ "$(wc -c < "$WORK/upload.b64")" -le 245760 ] || fail 'upload too large'
|
||||
b64dec "$WORK/upload.b64" "$WORK/import.gz" || fail 'invalid base64 upload'
|
||||
[ "$(wc -c < "$WORK/import.gz")" -le "$MAX_TAR" ] || fail 'archive too large'
|
||||
gzip -t "$WORK/import.gz" 2>/dev/null || fail 'invalid gzip archive'
|
||||
gzip -dc "$WORK/import.gz" | head -c 16777217 > "$WORK/import.tar"
|
||||
[ "$(wc -c < "$WORK/import.tar")" -le "$MAX_RAW" ] || fail 'unpacked backup exceeds 16 MiB'
|
||||
tar -tf "$WORK/import.tar" > "$WORK/entries" 2>/dev/null || fail 'invalid tar archive'
|
||||
[ -s "$WORK/entries" ] || fail 'empty archive'
|
||||
# Reject links, directories, devices and duplicate entries before extraction.
|
||||
tar -tvf "$WORK/import.tar" > "$WORK/types" 2>/dev/null || fail 'invalid tar headers'
|
||||
if grep -qv '^-' "$WORK/types"; then fail 'only regular files are allowed'; fi
|
||||
[ -z "$(sort "$WORK/entries" | uniq -d)" ] || fail 'duplicate archive entry'
|
||||
while IFS= read -r p; do
|
||||
case "$p" in
|
||||
etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config.dae|etc/daed/wing.db|etc/daed/wing.db-wal|etc/daed/wing.db-shm) ;;
|
||||
etc/dae/subscriptions/*.sub)
|
||||
name="${p#etc/dae/subscriptions/}"
|
||||
case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'invalid subscription path' ;; esac ;;
|
||||
*) fail 'unexpected archive entry' ;;
|
||||
esac
|
||||
done < "$WORK/entries"
|
||||
mkdir -p "$WORK/after"
|
||||
tar -xf "$WORK/import.tar" -C "$WORK/after"
|
||||
found=0
|
||||
for svc in dae daed daede; do
|
||||
[ -f "$WORK/after/etc/config/$svc" ] || continue
|
||||
found=1
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" -q export "$svc" >/dev/null || fail "invalid $svc configuration"
|
||||
done
|
||||
[ "$found" = 1 ] || fail 'backup has no UCI configuration'
|
||||
if [ -f "$WORK/after/etc/daed/wing.db-wal" ]; then
|
||||
[ -f "$WORK/after/etc/daed/wing.db" ] || fail 'database WAL has no database'
|
||||
fi
|
||||
ab="$(/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" -q get daede.config.active_backend || echo dae)"
|
||||
case "$ab" in dae|daed) ;; *) fail 'invalid backend in backup' ;; esac
|
||||
[ -x "/etc/init.d/$ab" ] || fail "backup backend $ab is not installed"
|
||||
}
|
||||
run_action() {
|
||||
trap finish EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
check_paths
|
||||
if [ "$ACTION" = import ]; then validate_archive; fi
|
||||
if [ "$ACTION" = reset ]; then
|
||||
mkdir -p "$WORK/after/etc/config"
|
||||
for svc in dae daed daede; do
|
||||
cp "$SHARE/defaults/$svc" "$WORK/after/etc/config/$svc"
|
||||
done
|
||||
"$SHARE/config-defaults.sh" "$WORK/after/etc/config"
|
||||
if [ ! -x /etc/init.d/daed ]; then
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" set daede.config.active_backend=dae
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" commit daede
|
||||
fi
|
||||
fi
|
||||
stop_backends
|
||||
snapshot
|
||||
if [ "$ACTION" = export ]; then
|
||||
paths "$WORK/before" > "$WORK/candidates"
|
||||
: > "$WORK/entries"
|
||||
while IFS= read -r p; do [ ! -f "$WORK/before/$p" ] || echo "$p" >> "$WORK/entries"; done < "$WORK/candidates"
|
||||
[ -s "$WORK/entries" ] || fail 'no configuration to export'
|
||||
tar -czf "$WORK/export.gz" -C "$WORK/before" -T "$WORK/entries"
|
||||
[ "$(wc -c < "$WORK/export.gz")" -le "$MAX_TAR" ] || fail 'config too large; use System Backup instead'
|
||||
b64enc "$WORK/export.gz"
|
||||
return
|
||||
fi
|
||||
MUTATING=1
|
||||
disable_backends
|
||||
# Old backups may omit an uninstalled backend. Keep its UCI file, but remove
|
||||
# all managed data first so stale WAL/SHM and local subscriptions cannot leak in.
|
||||
while IFS= read -r p; do
|
||||
case "$p" in etc/config/*) continue ;; esac
|
||||
rm -f "/$p"
|
||||
done < "$WORK/paths"
|
||||
paths "$WORK/after" > "$WORK/replacement"
|
||||
while IFS= read -r p; do
|
||||
[ -f "$WORK/after/$p" ] || continue
|
||||
mkdir -p "/${p%/*}"
|
||||
cp "$WORK/after/$p" "/$p"
|
||||
chmod 600 "/$p"
|
||||
done < "$WORK/replacement"
|
||||
if [ "$ACTION" = reset ]; then
|
||||
disable_backends
|
||||
sync_cron
|
||||
echo 'All defaults restored. Backends are disabled; configure before starting manually.'
|
||||
else
|
||||
# Preserve the imported active backend's enabled flag. The other backend
|
||||
# stays disabled even when the archive was made on a dual-backend router.
|
||||
for svc in dae daed; do
|
||||
[ "$svc" = "$ab" ] || [ ! -f "/etc/config/$svc" ] || { config -q set "$svc.config.enabled=0"; config -q commit "$svc"; }
|
||||
done
|
||||
sync_cron
|
||||
if [ "$(config -q get "$ab.config.enabled" || :)" = 1 ]; then
|
||||
/etc/init.d/"$ab" enable
|
||||
/etc/init.d/"$ab" start
|
||||
/etc/init.d/"$ab" running >/dev/null 2>&1 || fail 'restored backend failed to start'
|
||||
else
|
||||
/etc/init.d/"$ab" disable
|
||||
fi
|
||||
echo 'Backup restored. Active backend follows the saved enabled setting.'
|
||||
fi
|
||||
}
|
||||
|
||||
*)
|
||||
echo "usage: $0 export|import" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
case "$ACTION" in export|import|reset) ;; *) fail "usage: $0 export|import [upload]|reset" ;; esac
|
||||
# An atomic lock covers the complete operation, including background execution.
|
||||
mkdir "$LOCK" 2>/dev/null || fail 'another configuration operation is running'
|
||||
WORK="$(mktemp -d /tmp/daede-config.XXXXXX)" || { rmdir "$LOCK"; exit 1; }
|
||||
mkdir -p "$WORK/uci"
|
||||
if [ "$ACTION" = import ]; then
|
||||
case "$UPLOAD" in
|
||||
/tmp/daede-import.b64) ;;
|
||||
/tmp/daede-import.*.b64)
|
||||
token="${UPLOAD#/tmp/daede-import.}"; token="${token%.b64}"
|
||||
case "$token" in ''|*[!a-f0-9]*) rm -rf "$WORK"; rmdir "$LOCK"; fail 'invalid upload path' ;; esac ;;
|
||||
*) rm -rf "$WORK"; rmdir "$LOCK"; fail 'invalid upload path' ;;
|
||||
esac
|
||||
if [ ! -f "$UPLOAD" ] || [ -L "$UPLOAD" ]; then rm -rf "$WORK"; rmdir "$LOCK"; fail 'upload missing or unsafe'; fi
|
||||
mv "$UPLOAD" "$WORK/upload.b64" || { rm -rf "$WORK"; rmdir "$LOCK"; exit 1; }
|
||||
fi
|
||||
if [ "$ACTION" = export ]; then
|
||||
run_action
|
||||
else
|
||||
# Truncate before returning, so the frontend cannot mistake an earlier job
|
||||
# for this one. The child owns the lock and all cleanup from here on.
|
||||
: > "$LOG"
|
||||
(run_action) </dev/null >"$LOG" 2>&1 &
|
||||
echo "started in background, see $LOG"
|
||||
fi
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
# Shared installation/reset defaults. Optional directory is an isolated UCI tree.
|
||||
set -eu
|
||||
if [ "$#" -gt 0 ]; then
|
||||
config_dir="$1"
|
||||
mkdir -p "$config_dir/.uci"
|
||||
uci() { /sbin/uci -c "$config_dir" -t "$config_dir/.uci" "$@"; }
|
||||
fi
|
||||
|
||||
if ! uci -q get dae.config.lan_interface >/dev/null 2>&1; then
|
||||
uci -q set dae.config.lan_interface='br-lan'
|
||||
fi
|
||||
if ! uci -q show dae | grep -q "=group$"; then
|
||||
g="$(uci add dae group)"
|
||||
uci -q set "dae.$g.name=proxy"
|
||||
uci -q set "dae.$g.policy=min_moving_avg"
|
||||
fi
|
||||
if ! uci -q get dae.routing >/dev/null 2>&1; then
|
||||
uci -q set dae.routing=routing
|
||||
uci -q set dae.routing.private_direct=1
|
||||
uci -q set dae.routing.cn_direct=1
|
||||
uci -q set dae.routing.block_ads=0
|
||||
uci -q set dae.routing.fallback=proxy
|
||||
fi
|
||||
if ! uci -q get dae.dns >/dev/null 2>&1; then
|
||||
uci -q set dae.dns=dns
|
||||
uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53'
|
||||
uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53'
|
||||
uci -q set dae.dns.response_ttl='0'
|
||||
fi
|
||||
uci -q commit dae
|
||||
@@ -30,8 +30,7 @@
|
||||
"/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/refresh-index.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ]
|
||||
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
@@ -83,7 +82,11 @@
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-update.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-cron.sh enable": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-cron.sh disable": [ "exec" ]
|
||||
"/usr/share/luci-app-daede/daed-sub-cron.sh disable": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh reset": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh import /tmp/daede-import.*.b64": [ "exec" ],
|
||||
"/tmp/daede-import.*.b64": [ "write" ]
|
||||
},
|
||||
"uci": [ "dae", "daed", "daede" ]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user