🗽 Sync 2026-09-06 00:43:47
Merge-upstream / merge (push) Canceled after 0s

This commit is contained in:
github-actions[bot]
2026-09-06 00:43:48 +08:00
parent 259b690118
commit f638f12257
36 changed files with 1138 additions and 237 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dae
PKG_VERSION:=2026.08.28
PKG_RELEASE:=40
PKG_RELEASE:=41
PKG_SOURCE:=dae-src-2026.08.28-d6aca8a10b35.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
-1
View File
@@ -30,7 +30,6 @@ start_service() {
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
procd_set_param command "$PROG" run
procd_append_param command --config "$config_file"
procd_append_param command --disable-timestamp
procd_append_param command --logfile "$LOG_DIR/dae.log"
procd_append_param command --logfile-maxbackups "$log_maxbackups"
procd_append_param command --logfile-maxsize "$log_maxsize"
+61
View File
@@ -0,0 +1,61 @@
#
# Copyright (C) 2017-2024
#
# This is free software, licensed under the GNU General Public License v2.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser-q
PKG_VERSION:=1.5.6-stable
PKG_RELEASE=1
ifeq ($(ARCH),aarch64)
PKG_ARCH:=arm64
PKG_HASH:=skip
else ifeq ($(ARCH),arm)
PKG_ARCH:=armv7
PKG_HASH:=skip
else ifeq ($(ARCH),x86_64)
PKG_ARCH:=amd64
PKG_HASH:=skip
endif
include $(INCLUDE_DIR)/package.mk
define Package/filebrowser-q
SECTION:=net
CATEGORY:=Network
DEPENDS:=@(arm||aarch64||x86_64)
TITLE:=FileBrowser Quantum
endef
define Package/filebrowser-q/description
The best free self-hosted web-based file manager.
endef
PKG_SOURCE:=linux-$(PKG_ARCH)-filebrowser
PKG_SOURCE_URL:=https://github.com/gtsteffaniak/filebrowser/releases/download/v$(PKG_VERSION)
define Build/Prepare
$(call Build/Prepare/Default)
endef
define Build/Compile
endef
define Package/filebrowser-q/conffiles
/etc/filebrowser-q/
/etc/config/filebrowser-q
endef
define Package/filebrowser-q/install
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser-q
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q
$(INSTALL_DIR) $(1)/usr/bin/
$(INSTALL_BIN) $(DL_DIR)/linux-$(PKG_ARCH)-filebrowser $(1)/usr/bin/filebrowser-q
endef
$(eval $(call BuildPackage,filebrowser-q))
+13
View File
@@ -0,0 +1,13 @@
server:
port: 8989
database: "/etc/filebrowser-q/database.db"
sources:
- path: "/" # Do not use a root "/" directory or include the "/var" folder
config:
defaultEnabled: true
auth:
adminUsername: admin
adminPassword: "admin"
userDefaults:
ui:
locale: "zhCN"
+5
View File
@@ -0,0 +1,5 @@
config filebrowser 'config'
option enabled '0'
option listen_port '8989'
option root_path '/'
+60
View File
@@ -0,0 +1,60 @@
#!/bin/sh /etc/rc.common
START=99
STOP=10
CONF="filebrowser-q"
PROG="/usr/bin/filebrowser-q"
CONF_PATH="/etc/filebrowser-q/config.yaml"
PID_FILE="/var/run/filebrowser-q.pid"
start() {
config_load "$CONF"
local enabled
config_get_bool enabled "config" "enabled" "0"
[ "$enabled" -eq "1" ] || return 1
local listen_port root_path root_name
config_get listen_port "config" "listen_port" "8989"
config_get root_path "config" "root_path" "/"
# 路径为 / 时 name 设为 root,否则留空
root_name=""
[ "$root_path" = "/" ] && root_name="root"
if [ ! -f "$CONF_PATH" ]; then
mkdir -p "$(dirname "$CONF_PATH")"
cat <<EOF > "$CONF_PATH"
server:
port: $listen_port
database: "/etc/filebrowser-q/database.db"
sources:
- path: "$root_path"
name: "$root_name"
config:
defaultEnabled: true
auth:
adminUsername: "admin"
adminPassword: "admin"
userDefaults:
ui:
locale: "zhCN"
EOF
else
grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH"
sed -e "s/.*port:.*/ port: $listen_port/" \
-e "s,.*- path:.*, - path: \"$root_path\"," \
-e "s/.*name:.*/ name: \"$root_name\"/" \
-i "$CONF_PATH"
fi
echo "Starting filebrowser..."
start-stop-daemon -S -q -b -m -p "$PID_FILE" -x "$PROG" -- -c "$CONF_PATH"
}
stop() {
kill -9 `pidof filebrowser-q | sed "s/$$//g"` 2>/dev/null
rm -f "$PID_FILE"
echo "filebrowser stopped"
}
+6 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede
PKG_VERSION:=1.14.7
PKG_RELEASE:=41
PKG_RELEASE:=42
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
]),
+78
View File
@@ -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 "继续"
+78
View File
@@ -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).
@@ -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" ]
}
+11
View File
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: Apache-2.0
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI app for FileBrowser Quantum
LUCI_DEPENDS:=+filebrowser-q
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,88 @@
'use strict';
'require form';
'require poll';
'require rpc';
'require uci';
'require view';
'require fs';
function getServiceStatus() {
let isRunning = false;
return isRunning;
}
function renderStatus(isRunning, port) {
let spanTemp = '<span style="color:%s"><strong>%s %s</strong></span>';
let renderHTML;
if (isRunning) {
let button = String.format('&#160;<a class="btn cbi-button" href="http://%s:%s" target="_blank" rel="noreferrer noopener">%s</a>',
window.location.hostname, port, _('Open Web Interface'));
renderHTML = spanTemp.format('green', _('FileBrowser'), _('RUNNING')) + button;
} else {
renderHTML = spanTemp.format('red', _('FileBrowser'), _('NOT RUNNING'));
}
return renderHTML;
}
return view.extend({
load: async function () {
const promises = await Promise.all([
L.resolveDefault(fs.stat('/var/run/filebrowser-q.pid'), null),
uci.load('filebrowser-q')
]);
const data = {
isRunning: promises[0],
conf: promises[1]
};
return data;
},
render(data) {
let m, s, o;
let webport = (uci.get(data.conf, 'config', 'listen_port') || '8989');
m = new form.Map('filebrowser-q', _('FileBrowser Quantum'),
_('The best free self-hosted web-based file manager.') + '<br />'+
_('Default login username is %s and password is %s.').format('<code>admin</code>', '<code>admin</code>'));
s = m.section(form.TypedSection);
s.anonymous = true;
s.render = function() {
poll.add(function() {
return fs.stat('/var/run/filebrowser-q.pid').then(function(stat) {
let view = document.getElementById('service_status');
if (view) {
view.innerHTML = renderStatus(stat, webport);
}
}).catch(function() {
let view = document.getElementById('service_status');
if (view) {
view.innerHTML = renderStatus(null, webport);
}
});
});
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
E('p', { id: 'service_status' }, _('Collecting data...'))
]);
};
s = m.section(form.NamedSection, 'config', 'filebrowser');
o = s.option(form.Flag, 'enabled', _('Enable'));
o.default = o.disabled;
o.rmempty = false;
o = s.option(form.Value, 'listen_port', _('Listen port'));
o.datatype = 'port';
o.default = '8989';
o.rmempty = false;
o = s.option(form.Value, 'root_path', _('Root directory'));
o.default = '/';
o.rmempty = false;
return m.render();
}
});
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -0,0 +1,33 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: PACKAGE VERSION\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: zh-Hans\n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Collecting data..."
msgstr "正在收集数据中..."
msgid "Default login username is %s and password is %s."
msgstr "默认登录用户名为 %s,密码为 %s。"
msgid "Enable"
msgstr "启用"
msgid "Listen port"
msgstr "监听端口"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Open Web Interface"
msgstr "打开 Web 界面"
msgid "RUNNING"
msgstr "运行中"
msgid "Root directory"
msgstr "根目录"
@@ -0,0 +1,14 @@
{
"admin/services/filebrowser-q": {
"title": "FileBrowser Quantum",
"action": {
"order": 30,
"type": "view",
"path": "filebrowser-q"
},
"depends": {
"acl": [ "luci-app-filebrowser-q" ],
"uci": { "filebrowser-q": true }
}
}
}
@@ -0,0 +1,14 @@
{
"luci-app-filebrowser-q": {
"description": "Grant UCI access for luci-app-filebrowser-q",
"read": {
"ubus": {
"service": [ "list" ]
},
"uci": [ "filebrowser-q" ]
},
"write": {
"uci": [ "filebrowser-q" ]
}
}
}
@@ -0,0 +1,4 @@
{
"config": "filebrowser-q",
"init": "filebrowser-q"
}
@@ -92,16 +92,17 @@ elseif luci.http.formvalue("download") then
end
local inits, attr = {}
for i, f in ipairs(fs.glob("/tmp/upload/*")) do
for f in fs.glob("/tmp/upload/*") do
attr = fs.stat(f)
if attr then
inits[i] = {}
inits[i].name = fs.basename(f)
inits[i].mtime = os.date("%Y-%m-%d %H:%M:%S", attr.mtime)
inits[i].modestr = attr.modestr
inits[i].size = tostring(attr.size)
inits[i].remove = 0
inits[i].install = false
inits[#inits + 1] = {
name = fs.basename(f),
mtime = os.date("%Y-%m-%d %H:%M:%S", attr.mtime),
modestr = attr.modestr,
size = tostring(attr.size),
remove = 0,
install = false
}
end
end
+2 -2
View File
@@ -11,8 +11,8 @@ LUCI_DESCRIPTION:=Live firewall log table (nftables/fw4 and iptables LOG) with f
LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.37
PKG_RELEASE:=7
PKG_VERSION:=0.1.38
PKG_RELEASE:=8
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
@@ -7,7 +7,7 @@
*/
return baseclass.extend({
/* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */
APP_VERSION: '0.1.37',
APP_VERSION: '0.1.38',
ROW_LIMIT_OPTIONS: [ 25, 50, 100, 250, 500, 1000, 2000 ],
DEFAULT_ROW_LIMIT: 100,
/* Row pass/deny tint (#40): classic green/red default; accessible teal/orange */
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.9.2
PKG_RELEASE:=110
PKG_RELEASE:=111
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -47,6 +47,11 @@ function waitForElement(selector, callback) {
observer.observe(document.body, { childList: true, subtree: true });
}
function waitForElementId(id, callback) {
waitForElement("#" + CSS.escape(id), callback);
//waitForElement('[id="' + id + '"]', callback);
}
function get_current_url() {
return window.location.origin + window.location.pathname;
}
@@ -221,6 +221,16 @@ o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends({ node = "", ['!reverse'] = true })
o = s:option(DummyValue, "_is_singbox", "")
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends("_hide", "1")
o = s:option(DummyValue, "_is_xray", "")
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends("_hide", "1")
o = s:option(Flag, "log", translate("Enable Node Log"))
o:depends("_node", "1")
@@ -249,7 +259,7 @@ o:depends("log", "1")
o = s:option(DummyValue, "_show_dns_option", "")
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends("_hide", "1")
o:depends({ mode = "1", _node = "1" })
o = s:option(ListValue, "direct_dns_query_strategy", translate("Direct Query Strategy"))
o.default = "UseIP"
@@ -262,11 +272,9 @@ o = s:option(ListValue, "remote_dns_protocol", translate("Remote DNS Protocol"))
o:value("tcp", "TCP")
o:value("doh", "DoH")
o:value("udp", "UDP")
if current_node.type == "sing-box" then
o:value("tls", "TLS(DoT)")
o:value("quic", "QUIC(DoQ)")
o:value("http3", "HTTP3(DoH3)")
end
o:value("tls", "TLS(DoT)", { _is_singbox = "1" })
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" })
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" })
o:depends("_show_dns_option", "1")
---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ)
@@ -307,44 +315,30 @@ o = s:option(Value, "remote_dns_client_ip", translate("Remote DNS EDNS Client Su
o.description = translate("Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address).") .. "<br />" ..
translate("This feature requires the DNS server to support the Edns Client Subnet (RFC7871).")
o.datatype = "ipaddr"
o:depends("remote_dns_protocol", "tcp")
o:depends("remote_dns_protocol", "doh")
o:depends("remote_dns_protocol", "udp")
o:depends("remote_dns_protocol", "http3")
o:depends("remote_dns_protocol", "quic")
o:depends("remote_dns_protocol", "tls")
o:depends("_show_dns_option", "1")
o = s:option(ListValue, "remote_dns_detour", translate("Remote DNS Outbound"))
o.default = "remote"
o:value("remote", translate("Remote"))
o:value("direct", translate("Direct"))
o:depends("remote_dns_protocol", "tcp")
o:depends("remote_dns_protocol", "doh")
o:depends("remote_dns_protocol", "udp")
o:depends("remote_dns_protocol", "http3")
o:depends("remote_dns_protocol", "quic")
o:depends("remote_dns_protocol", "tls")
o:depends("_show_dns_option", "1")
o = s:option(Flag, "remote_fakedns", "FakeDNS", translate("Use FakeDNS work in the domain that proxy."))
o.default = "0"
o.rmempty = false
o:depends("_hide", "1")
o = s:option(ListValue, "remote_dns_query_strategy", translate("Remote Query Strategy"))
o.default = "UseIPv4"
o:value("UseIP")
o:value("UseIPv4")
o:value("UseIPv6")
o:depends("remote_dns_protocol", "tcp")
o:depends("remote_dns_protocol", "doh")
o:depends("remote_dns_protocol", "udp")
o:depends("remote_dns_protocol", "http3")
o:depends("remote_dns_protocol", "quic")
o:depends("remote_dns_protocol", "tls")
o:depends("_show_dns_option", "1")
o = s:option(Value, "remote_rewrite_ttl", translate("Remote DNS") .. " TTL")
o.datatype = "min(1)"
o.default = "30"
o:depends("_hide", "1")
o:depends({ _show_dns_option = "1", _is_singbox = "1" })
o = s:option(ListValue, "dns_hosts_mode", translate("Domain Override"))
o:value("default", translate("Use global config"))
@@ -368,22 +362,39 @@ end
local o_node = s.fields["node"]
local shunt_list = {}
for k, v in pairs(nodes_table) do
o_node:value(v.id, v["remark"])
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
if v.type == "Xray" then
--s.fields["_is_xray"]:depends({ node = v.id })
end
if v.type == "sing-box" then
s.fields["remote_rewrite_ttl"]:depends({ node = v.id })
s.fields["_is_singbox"]:depends({ node = v.id })
end
s.fields["_show_dns_option"]:depends({ mode = "1", node = v.id })
if v.node_type == "normal" or v.protocol == "_balancing" or v.protocol == "_urltest" then
--Shunt node has its own separate options.
s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "tcp" })
s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "doh" })
s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "udp" })
s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "http3" })
s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "quic" })
s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "tls" })
s.fields["remote_fakedns"]:depends({ node = v.id, _show_dns_option = "1" })
end
if v.protocol and v.protocol == "_shunt" then
shunt_list[#shunt_list + 1] = v
end
end
--m:appendTemplate("/acl/options", {section = arg[1]})
-- Shunt Start
if current_node.protocol == "_shunt" then
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s, {
s_cfgid = s.section,
node_id = current_node_id,
node = current_node,
verify_option = s.fields["node"]
})
end
m:appendTemplate("/acl/shunt", { shunt_list = api.jsonc.stringify(shunt_list), section = s.section })
return api.return_map(m)
@@ -85,6 +85,16 @@ o.template = m:template_path("/cbi/nodes_listvalue")
o:value("", translate("Close"))
o.group = {""}
o = s:taboption("Main", DummyValue, "_node", "")
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends({ node = "", ['!reverse'] = true })
o = s:taboption("Main", DummyValue, "_is_singbox", "")
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends("_hide", "1")
current_node_id = m:get(s.section, "node")
current_node = current_node_id and m:get(current_node_id) or {}
@@ -113,7 +123,7 @@ if (has_singbox or has_xray) and #nodes_table > 0 then
tips.cfgvalue = function(t, n)
return string.format('<a style="color: red">%s</a>', translate("There are no available nodes, please add or subscribe nodes first."))
end
tips:depends({ node = "", ["!reverse"] = true })
tips:depends("_node", "1")
for k, v in pairs(shunt_list) do
tips:depends("node", v.id)
end
@@ -157,10 +167,11 @@ end
node_socks_port = s:taboption("Main", Value, "node_socks_port", translate("Node") .. " Socks " .. translate("Listen Port"))
node_socks_port.default = 1070
node_socks_port.datatype = "port"
node_socks_port:depends("_node", "1")
node_socks_bind_local = s:taboption("Main", Flag, "node_socks_bind_local", translate("Node") .. " Socks " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
node_socks_bind_local.default = "1"
node_socks_bind_local:depends({ node = "", ["!reverse"] = true })
node_socks_bind_local:depends("_node", "1")
s:tab("DNS", translate("DNS"))
@@ -190,11 +201,9 @@ o = s:taboption("DNS", ListValue, "remote_dns_protocol", translate("Remote DNS P
o:value("tcp", "TCP")
o:value("doh", "DoH")
o:value("udp", "UDP")
if current_node.type == "sing-box" then
o:value("tls", "TLS(DoT)")
o:value("quic", "QUIC(DoQ)")
o:value("http3", "HTTP3(DoH3)")
end
o:value("tls", "TLS(DoT)", { _is_singbox = "1" })
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" })
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" })
---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ)
o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS"))
@@ -388,6 +397,9 @@ for k, v in pairs(nodes_table) do
if #normal_list == 0 and #iface_list == 0 then
break
end
if v.type == "sing-box" then
s.fields["_is_singbox"]:depends({ node = v.id })
end
o_node:value(v.id, v["remark"])
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
o_socks:value(v.id, v["remark"])
@@ -4,6 +4,7 @@ if not data.node_id or not data.node then
return
end
local api = m.api
local s_cfgid = data.s_cfgid
local current_node_id = data.node_id
local node_list = data.node_list or api.get_node_list()
@@ -0,0 +1,56 @@
<%
local map = self.map
local api = map.api
local config = map.config
local section = self.section
-%>
<script type="text/javascript">
//<![CDATA[
function setOption(option, val) {
const dom = document.getElementById('cbid.<%=config%>.<%=section%>.' + option);
if (dom) {
dom.value = val;
}
const combobox_dom = document.getElementById('cbi.combobox.cbid.<%=config%>.<%=section%>.' + option);
if (combobox_dom) {
combobox_dom.value = val;
//combobox_dom.selectedIndex = val;
//combobox_dom.dispatchEvent(new Event("change", { bubbles: true }));
}
}
document.addEventListener("DOMContentLoaded", function () {
function dom_event() {
waitForElementId('cbi.combobox.cbid.<%=config%>.<%=section%>.tcp_no_redir_ports', function(el) {
const o_val = el.value;
el.addEventListener("change", () => {
const udp_no_redir_ports = getOption("<%=config%>", "<%=section%>", "udp_no_redir_ports");
if (el.value == "1:65535" && udp_no_redir_ports && udp_no_redir_ports.value == "1:65535") {
setOption("tcp_no_redir_ports", "");
setOption("udp_no_redir_ports", "");
setOption("mode", 0);
cbi_d_update();
}
});
});
waitForElementId('cbi.combobox.cbid.<%=config%>.<%=section%>.udp_no_redir_ports', function(el) {
const o_val = el.value;
el.addEventListener("change", () => {
const tcp_no_redir_ports = getOption("<%=config%>", "<%=section%>", "tcp_no_redir_ports");
if (el.value == "1:65535" && tcp_no_redir_ports && tcp_no_redir_ports.value == "1:65535") {
setOption("tcp_no_redir_ports", "");
setOption("udp_no_redir_ports", "");
setOption("mode", 0);
cbi_d_update();
}
});
});
}
const ori_cbi_d_update = cbi_d_update;
cbi_d_update = function() {
ori_cbi_d_update();
dom_event();
};
dom_event();
});
//]]>
</script>
@@ -0,0 +1,38 @@
<%
local map = self.map
local api = map.api
local appname = map.config
local section = self.section
-%>
<script type="text/javascript">
//<![CDATA[
const shunt_list = JSON.parse('<%=self.shunt_list%>');
document.addEventListener("DOMContentLoaded", () => setTimeout(() => {
const hiddenSelect = document.getElementById("cbid.<%=appname%>.<%=section%>.node");
let o_val = hiddenSelect.value
const o_hasItem = shunt_list.find(element => element.id == o_val);
hiddenSelect.addEventListener("change", function(el){
let new_val = el.target.value
let new_hasItem = shunt_list.find(element => element.id == new_val);
new_hasItem = true;
if (new_hasItem) {
XHR.get('<%=api.url("update_config")%>', {
id: "<%=section%>",
data: JSON.stringify({
node: new_val
})
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
window.location.reload();
}
else {
alert("<%:Error%>");
}
});
}
});
}, 100));
//]]>
</script>
@@ -419,10 +419,10 @@ table td, .table .td {
<%- else %>
(function() {
if (typeof(cbi_t_switch) === "function") {
var old_switch = cbi_t_switch;
const ori_cbi_t_switch = cbi_t_switch;
cbi_t_switch = function(section, tab) {
dechecked_all_node();
return old_switch(section, tab);
return ori_cbi_t_switch(section, tab);
};
}
})();
@@ -1521,22 +1521,47 @@ table td, .table .td {
}
});
}
waitForElement('input[type="checkbox"][name*="<%=appname%>"][name*="show_node_info"]', function(el) {
el.addEventListener("change", () => {
el.blur();
show_node_info = el.checked ? "1" : "0";
onChange("show_node_info", show_node_info);
function dom_event() {
waitForElement('input[type="checkbox"][name*="<%=appname%>"][name*="show_node_info"]', function(el) {
el.addEventListener("change", () => {
el.blur();
show_node_info = el.checked ? "1" : "0";
onChange("show_node_info", show_node_info);
});
});
});
waitForElement('select[name*="<%=appname%>"][name*="auto_detection_time"]', function(el) {
el.addEventListener("change", () => {
el.blur();
auto_detection_time = el.value;
onChange("auto_detection_time", auto_detection_time);
waitForElement('select[name*="<%=appname%>"][name*="auto_detection_time"]', function(el) {
el.addEventListener("change", () => {
el.blur();
auto_detection_time = el.value;
onChange("auto_detection_time", auto_detection_time);
});
});
});
waitForElement('select[id*="<%=appname%>"][id*="url_test_url"]', function(el) {
el.addEventListener("change", () => {
if (el.value) {
onChange("url_test_url", el.value);
}
});
});
waitForElement('input[id*="<%=appname%>"][id*="url_test_url"]', function(el) {
el.addEventListener("change", () => {
if (el.value) {
onChange("url_test_url", el.value);
}
});
});
}
const ori_cbi_d_update = cbi_d_update;
cbi_d_update = function() {
ori_cbi_d_update();
dom_event();
};
dom_event();
const links = document.querySelectorAll('a');
links.forEach(link => {
@@ -111,7 +111,7 @@ api.uci_foreach_c("haproxy_config", function(t)
if server_node.type ~= "Socks" then
local relay_port = server_node.port
local new_port = api.get_new_port()
local config_file = string.format("haproxy_%s_%s.json", t[".name"], new_port)
local config_file = string.format("%s_%s.json", t[".name"], new_port)
sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null',
appname,
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s",
@@ -3,7 +3,7 @@
. /usr/share/passwall2/utils.sh
LOCK_FILE=${LOCK_PATH}/${CONFIG}_lease2hosts.lock
LEASE_FILE="/tmp/dhcp.leases"
LEASE_FILE=$(config_t_get dnsmasq leasefile "/tmp/dhcp.leases")
HOSTS_FILE="$TMP_PATH2/dhcp-hosts"
TMP_FILE="/tmp/dhcp-hosts.tmp"
@@ -15,7 +15,7 @@ fi
reload_dnsmasq_pids() {
local pidfile pid
find $TMP_PATH/acl -type f -name 'dnsmasq.pid' 2>/dev/null | while read pidfile; do
find $TMP_PATH/acl -type f -name '*_dnsmasq.pid' 2>/dev/null | while read pidfile; do
if [ -s "$pidfile" ]; then
read pid < "$pidfile"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then