mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
config filebrowser 'config'
|
||||
option enabled '0'
|
||||
option listen_port '8989'
|
||||
option root_path '/'
|
||||
|
||||
@@ -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,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
|
||||
]),
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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" ]
|
||||
}
|
||||
|
||||
@@ -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(' <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();
|
||||
}
|
||||
});
|
||||
Symlink
+1
@@ -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
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user