Compare commits

...
4 Commits
Author SHA1 Message Date
action ec44f6fe2e update 2026-09-06 05:51:06
marry-jell / merge (push) Canceled after 0s
2026-09-06 05:51:06 +08:00
action 37eb246de8 update 2026-09-06 01:51:51 2026-09-06 01:51:51 +08:00
action 47831f020d update 2026-09-05 23:35:35 2026-09-05 23:35:35 +08:00
cwx 02766f9a2f Remove 'luci-app-daede' from git_sparse_clone 2026-09-05 23:35:06 +08:00
46 changed files with 922 additions and 565 deletions
+1 -1
View File
@@ -178,7 +178,7 @@ jobs:
git_sparse_clone master https://github.com/fengqi/luci-app-uugamebooster \ git_sparse_clone master https://github.com/fengqi/luci-app-uugamebooster \
uugamebooster luci-app-uugamebooster uugamebooster luci-app-uugamebooster
git_sparse_clone master https://github.com/kenzok8/small \ git_sparse_clone master https://github.com/kenzok8/small \
clashoo luci-app-clashoo luci-app-daede clashoo luci-app-clashoo
) & ) &
( (
git_clone https://github.com/muink/luci-app-dnsproxy git_clone https://github.com/muink/luci-app-dnsproxy
+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dae PKG_NAME:=dae
PKG_VERSION:=2026.08.28 PKG_VERSION:=2026.08.28
PKG_RELEASE:=3 PKG_RELEASE:=4
PKG_SOURCE:=dae-src-2026.08.28-d6aca8a10b35.tar.gz PKG_SOURCE:=dae-src-2026.08.28-d6aca8a10b35.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src 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 env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
procd_set_param command "$PROG" run procd_set_param command "$PROG" run
procd_append_param command --config "$config_file" 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 "$LOG_DIR/dae.log"
procd_append_param command --logfile-maxbackups "$log_maxbackups" procd_append_param command --logfile-maxbackups "$log_maxbackups"
procd_append_param command --logfile-maxsize "$log_maxsize" procd_append_param command --logfile-maxsize "$log_maxsize"
+78
View File
@@ -0,0 +1,78 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser-q
PKG_VERSION:=1.5.6-stable
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/filebrowser-$(PKG_VERSION)
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=kiddin9
PKG_BUILD_DEPENDS:=golang/host node/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/gtsteffaniak/filebrowser/backend
GO_PKG_BUILD_DIR:=$(PKG_BUILD_DIR)/backend
GO_PKG_LDFLAGS_X:= \
$(GO_PKG)/internal/version.Version=v$(PKG_VERSION) \
$(GO_PKG)/internal/version.CommitSHA=$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/filebrowser-q
SECTION:=utils
CATEGORY:=Utilities
SUBMENU:=Filesystem
TITLE:=FileBrowser Quantum - Modern Web File Manager
URL:=https://github.com/gtsteffaniak/filebrowser
DEPENDS:=$(GO_ARCH_DEPENDS)
endef
define Package/filebrowser-q/description
FileBrowser Quantum provides a modern, responsive web-based file management
interface with multi-source, real-time search, and enhanced preview features.
endef
define Package/filebrowser-q/conffiles
/etc/filebrowser-q/
/etc/config/filebrowser-q
endef
define Build/Prepare
$(call Build/Prepare/Default)
endef
define Build/Compile
( \
pushd $(PKG_BUILD_DIR)/frontend && \
npm install && \
npm run build ; \
)
( \
cd $(PKG_BUILD_DIR)/backend && \
$(GO_PKG_VARS) \
go build \
-trimpath \
-ldflags="-w -s" \
-o $(PKG_BUILD_DIR)/filebrowser . ; \
)
endef
define Package/filebrowser-q/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/filebrowser $(1)/usr/bin/filebrowser-q
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/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" "8787"
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"
}
+1
View File
@@ -37,6 +37,7 @@ define KernelPackage/mt7603e
CATEGORY:=Kernel modules CATEGORY:=Kernel modules
TITLE:=MTK wifi AP driver TITLE:=MTK wifi AP driver
DEPENDS:=@TARGET_ramips DEPENDS:=@TARGET_ramips
CONFLICTS:=kmod-mt7603
FILES:=$(PKG_BUILD_DIR)/mt7603_wifi_ap/mt7603e.ko FILES:=$(PKG_BUILD_DIR)/mt7603_wifi_ap/mt7603e.ko
SUBMENU:=Wireless Drivers SUBMENU:=Wireless Drivers
MENU:=1 MENU:=1
+1
View File
@@ -25,6 +25,7 @@ define KernelPackage/mt76x2e
CATEGORY:=Kernel modules CATEGORY:=Kernel modules
TITLE:=MTK MT76x2e wifi AP driver TITLE:=MTK MT76x2e wifi AP driver
DEPENDS:=@TARGET_ramips DEPENDS:=@TARGET_ramips
CONFLICTS:=kmod-mt76x2
FILES:=$(PKG_BUILD_DIR)/mt76x2_ap/mt76x2_ap.ko FILES:=$(PKG_BUILD_DIR)/mt76x2_ap/mt76x2_ap.ko
SUBMENU:=Wireless Drivers SUBMENU:=Wireless Drivers
MENU:=1 MENU:=1
+6 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede PKG_NAME:=luci-app-daede
PKG_VERSION:=1.14.7 PKG_VERSION:=1.14.7
PKG_RELEASE:=24 PKG_RELEASE:=25
PKG_MAINTAINER:=kenzok8 PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) 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/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/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-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/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/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 $(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 ui';
'require view'; 'require view';
'require view.daede.backend as backend'; 'require view.daede.backend as backend';
'require view.daede.styles as styles';
const MAX_LINES = 5000; 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" ... */ /* 拆字段: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_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_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) { function detectLevel(line) {
// daed/dae logs use lvl=info / [INFO] / level=warning style // daed/dae logs use lvl=info / [INFO] / level=warning style
@@ -117,13 +119,21 @@ function parseLine(line) {
} }
m = line.match(RE_PREFIXED_LINE); m = line.match(RE_PREFIXED_LINE);
if (!m) return null; let ts = '', lvl = '', body = '';
if (m) {
const body = m[3] || ''; 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.-]*=)/); const kvStart = body.search(/(?:^|\s)(?=[A-Za-z_][\w.-]*=)/);
return { return {
ts: formatTs(m[1]), ts: ts,
lvl: m[2], lvl: lvl,
msg: kvStart === -1 ? body : body.slice(0, kvStart).trimEnd(), msg: kvStart === -1 ? body : body.slice(0, kvStart).trimEnd(),
kv: kvStart === -1 ? '' : body.slice(kvStart).trim() kv: kvStart === -1 ? '' : body.slice(kvStart).trim()
}; };
@@ -135,11 +145,10 @@ function buildLine(ln) {
if (!parsed) { if (!parsed) {
return E('div', { 'class': 'dd-line ' + cls }, ln); return E('div', { 'class': 'dd-line ' + cls }, ln);
} }
const parts = [ const parts = [];
E('span', { 'class': 'dd-ts' }, parsed.ts), if (parsed.ts) parts.push(E('span', { 'class': 'dd-ts' }, parsed.ts));
E('span', { 'class': 'dd-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)), parts.push(E('span', { 'class': 'dd-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)));
E('span', { 'class': 'dd-msg' }, parsed.msg) parts.push(E('span', { 'class': 'dd-msg' }, parsed.msg));
];
if (parsed.kv) parts.push(E('span', { 'class': 'dd-kv' }, parsed.kv)); if (parsed.kv) parts.push(E('span', { 'class': 'dd-kv' }, parsed.kv));
return E('div', { 'class': 'dd-line ' + cls }, parts); return E('div', { 'class': 'dd-line ' + cls }, parts);
} }
@@ -158,7 +167,8 @@ return view.extend({
paused: false, paused: false,
autoScroll: true, autoScroll: true,
filter: '', filter: '',
userScrolled: false userScrolled: false,
reading: false
}; };
const pane = E('div', { 'class': 'dd-log-pane', 'id': 'dd-log-pane' }, [ 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 meta = E('span', { 'class': 'dd-log-meta' }, '');
const cbAuto = E('input', { 'type': 'checkbox', 'checked': 'checked' }); const btnAuto = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-auto' });
cbAuto.addEventListener('change', function() { const btnPause = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-pause' });
state.autoScroll = cbAuto.checked; 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) { if (state.autoScroll) {
pane.scrollTop = pane.scrollHeight; pane.scrollTop = pane.scrollHeight;
state.userScrolled = false; state.userScrolled = false;
} }
syncControls();
}); });
btnPause.addEventListener('click', function() {
const cbPause = E('input', { 'type': 'checkbox' }); state.paused = !state.paused;
cbPause.addEventListener('change', function() { syncControls();
state.paused = cbPause.checked; if (!state.paused) tick();
}); });
syncControls();
const selFilter = E('select', { 'class': 'dd-log-btn' }, [ const selFilter = E('select', { 'class': 'dd-log-btn' }, [
E('option', { 'value': '' }, _('All')), E('option', { 'value': '' }, _('All')),
@@ -287,9 +307,11 @@ return view.extend({
} }
function tick() { 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) { return fs.stat(LOG_PATH).then(function(st) {
if (state.paused) return;
const size = st.size || 0; const size = st.size || 0;
if (size === state.lastSize) { if (size === state.lastSize) {
meta.textContent = '%d bytes · live'.format(size); meta.textContent = '%d bytes · live'.format(size);
@@ -299,6 +321,7 @@ return view.extend({
// File rotated/truncated → full reload // File rotated/truncated → full reload
const rotated = size < state.lastSize; const rotated = size < state.lastSize;
return fs.read_direct(LOG_PATH, 'text').then(function(content) { return fs.read_direct(LOG_PATH, 'text').then(function(content) {
if (state.paused) return;
content = content || ''; content = content || '';
let delta; let delta;
if (rotated || state.lastContent === '') { if (rotated || state.lastContent === '') {
@@ -323,6 +346,7 @@ return view.extend({
pane.scrollTop = pane.scrollHeight; pane.scrollTop = pane.scrollHeight;
}); });
}).catch(function(e) { }).catch(function(e) {
if (state.paused) return;
const msg = String(e); const msg = String(e);
if (msg.indexOf('NotFoundError') !== -1 || msg.indexOf('No such') !== -1) if (msg.indexOf('NotFoundError') !== -1 || msg.indexOf('No such') !== -1)
renderEmpty(_('Log file does not exist yet.')); renderEmpty(_('Log file does not exist yet.'));
@@ -331,15 +355,15 @@ return view.extend({
state.lastSize = -1; state.lastSize = -1;
state.lastContent = ''; state.lastContent = '';
meta.textContent = ''; meta.textContent = '';
}); }).finally(function() { state.reading = false; });
} }
poll.add(tick); poll.add(tick);
tick(); tick();
const toolbarItems = [ const toolbarItems = [
E('label', {}, [ cbAuto, _('Auto-scroll') ]), btnAuto,
E('label', {}, [ cbPause, _('Pause') ]), btnPause,
selFilter, selFilter,
btnClear, btnClear,
btnDownload, btnDownload,
@@ -350,7 +374,7 @@ return view.extend({
const toolbar = E('div', { 'class': 'dd-log-toolbar' }, toolbarItems); const toolbar = E('div', { 'class': 'dd-log-toolbar' }, toolbarItems);
return E('div', { 'class': 'dd-log-wrap' }, [ return E('div', { 'class': 'dd-log-wrap' }, [
E('style', {}, CSS), E('style', {}, CSS + styles.CSS),
E('div', { 'class': 'dd-log-card' }, [ E('div', { 'class': 'dd-log-card' }, [
E('h4', { 'class': 'dd-log-card-title' }, _('%s Realtime Log').format(ctx.name)), E('h4', { 'class': 'dd-log-card-title' }, _('%s Realtime Log').format(ctx.name)),
toolbar, toolbar,
@@ -4,6 +4,9 @@
'require baseclass'; 'require baseclass';
const CSS = [ 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-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)}', '.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 /* padding:0 neutralizes Argon's h4{padding:.75rem 1.25rem}, which otherwise
@@ -7,6 +7,7 @@
'require ui'; 'require ui';
'require view'; 'require view';
'require view.daede.backend as backend'; 'require view.daede.backend as backend';
'require view.daede.daed-session as daedSession';
const DATA_PATHS = { const DATA_PATHS = {
geoip: '/usr/share/v2ray/geoip.dat', 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:hover{background:rgba(128,128,128,.12)}',
'.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}', '.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}',
'.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}', '.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{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 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%}', '.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'); return runJob('update-pkg.sh', pkg, btn, '/tmp/luci-app-daede.pkg-' + pkg + '.log');
}; };
// === Config backup (export / import the whole daede config) === // Configuration operations share a backend lock and one UI busy state.
const exportBtn = E('button', { 'class': 'dd-up-btn' }, _('Export')); const backupScript = '/usr/share/luci-app-daede/config-backup.sh';
const importBtn = E('button', { 'class': 'dd-up-btn' }, _('Import')); 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' }); const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' });
let backupBusy = false;
exportBtn.addEventListener('click', function() { const showBackupError = function(e) {
const orig = exportBtn.textContent; ui.addNotification(null, E('p', {}, e.message || String(e)), 'error');
exportBtn.disabled = true; exportBtn.textContent = '...'; };
fs.exec('/usr/share/luci-app-daede/config-backup.sh', ['export']).then(function(res) { const configOperation = function(button, operation) {
if (res.code !== 0 || !res.stdout) { if (backupBusy) return Promise.resolve();
logPane.textContent = String(res.stderr || res.stdout || 'export failed').trim(); 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'); logPane.classList.add('show');
return; if (/^✗/m.test(content)) throw new Error(_('Configuration operation failed. See the log below.'));
} if (/^✓/m.test(content)) return;
let bin; if (++tries > 90) throw new Error(_('Operation is still running. Check the log before retrying.'));
try { return new Promise(function(resolve) { setTimeout(resolve, 2000); }).then(check);
bin = atob(res.stdout.trim()); });
} catch (e) { };
logPane.textContent = _('Export failed: invalid base64 data from server'); return check();
logPane.classList.add('show'); };
return; const reloadConfig = function(message) {
} daedSession.clear(window.localStorage);
if (!bin || bin.length === 0) { ['dae', 'daed', 'daede'].forEach(function(name) { uci.unload(name); });
logPane.textContent = _('Export failed: empty archive'); ui.showModal(_('Configuration restored'), [
logPane.classList.add('show'); E('p', {}, message),
return; E('div', { 'class': 'right' }, E('button', {
} 'class': 'cbi-button cbi-button-positive', 'click': function() { window.location.reload(); }
const arr = new Uint8Array(bin.length); }, _('Reload Page')))
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' }); exportBtn.addEventListener('click', async function() {
document.body.appendChild(a); a.click(); document.body.removeChild(a); if (!await confirmBackup(_('Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?'))) return;
setTimeout(function() { URL.revokeObjectURL(url); }, 1000); configOperation(exportBtn, function() {
}).catch(function(e) { return execBackup(['export']).then(function(res) {
logPane.textContent = _('Export failed') + ': ' + (e ? (e.message || String(e)) : _('script not found')); const bin = atob((res.stdout || '').trim());
logPane.classList.add('show'); if (!bin) throw new Error(_('Export failed: empty archive'));
}).finally(function() { const bytes = Uint8Array.from(bin, function(c) { return c.charCodeAt(0); });
exportBtn.disabled = false; exportBtn.textContent = orig; 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(); }); 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]; const file = ev.target.files && ev.target.files[0];
if (!file) return; 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; fileInput.value = ''; return;
} }
const reader = new FileReader(); 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?'))) {
reader.onload = function(e) { fileInput.value = ''; return;
const b64 = String(e.target.result).split(',')[1] || ''; }
fs.write('/tmp/daede-import.b64', b64).then(function() { configOperation(importBtn, function() {
return runJob('config-backup.sh', 'import', importBtn, '/tmp/luci-app-daede.backup.log'); return new Promise(function(resolve, reject) {
}).catch(function(e) { const reader = new FileReader();
logPane.textContent = _('Import failed') + ': ' + (e ? (e.message || String(e)) : _('unknown error')); reader.onerror = function() { reject(new Error(_('Unable to read backup file.'))); };
logPane.classList.add('show'); reader.onload = function() { resolve(String(reader.result).split(',')[1] || ''); };
}).finally(function() { fileInput.value = ''; }); reader.readAsDataURL(file);
}; }).then(function(b64) {
reader.readAsDataURL(file); // 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() { const refresh = function() {
@@ -508,12 +573,15 @@ return view.extend({
]), ]),
E('div', { 'class': 'dd-card' }, [ E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Config Backup')), E('h4', { 'class': 'dd-card-title' }, _('Config Backup')),
E('div', { 'class': 'dd-up-row' }, [ E('div', { 'class': 'dd-up-row dd-backup-row' }, [
E('span', { 'class': 'dd-up-icon dd-up-new' }, ''), E('span', { 'class': 'dd-up-icon dd-up-new' }, ''),
E('span', { 'class': 'dd-up-name' }, _('dae + daed')), E('span', { 'class': 'dd-up-name' }, _('dae + daed')),
E('span', { 'class': 'dd-up-meta' }, _('Back up / restore the whole daede config (kernels excluded)')), E('span', {
exportBtn, 'class': 'dd-up-meta',
importBtn '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 fileInput
]), ]),
+78
View File
@@ -1025,6 +1025,12 @@ msgstr "导出"
msgid "Import" msgid "Import"
msgstr "导入" 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)" msgid "Back up / restore the whole daede config (kernels excluded)"
msgstr "备份 / 还原整个 daede 配置(不含内核)" msgstr "备份 / 还原整个 daede 配置(不含内核)"
@@ -1086,3 +1092,75 @@ msgid ""
"Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set " "Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set "
"it, e.g. eth0.2." "it, e.g. eth0.2."
msgstr "上行 WAN 接口。auto 按默认路由自动检测;老式 swconfig 交换机或多 WAN 时手动指定,例如 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" msgid "Import"
msgstr "导入" 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)" msgid "Back up / restore the whole daede config (kernels excluded)"
msgstr "备份 / 还原整个 daede 配置(不含内核)" msgstr "备份 / 还原整个 daede 配置(不含内核)"
@@ -1086,3 +1092,75 @@ msgid ""
"Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set " "Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set "
"it, e.g. eth0.2." "it, e.g. eth0.2."
msgstr "上行 WAN 接口。auto 按默认路由自动检测;老式 swconfig 交换机或多 WAN 时手动指定,例如 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, # Idempotent: only add what's missing. Lives in the `dae` UCI package,
# consumed by gen-dae-config.sh. # consumed by gen-dae-config.sh.
if [ -f /etc/config/dae ]; then if [ -f /etc/config/dae ]; then
if ! uci -q get dae.config.lan_interface >/dev/null 2>&1; then /usr/share/luci-app-daede/config-defaults.sh
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
# Migrate legacy group filters to the unified source / name_filter fields, # 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 # 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). # fields used to re-populate the new ones and looked un-deletable).
+241 -62
View File
@@ -1,71 +1,250 @@
#!/bin/sh #!/bin/sh
# config-backup.sh export|import — back up/restore the whole daede config # Whole-plugin backup/import/reset. Only fixed configuration paths are managed.
# (dae + daed + active backend). export: prints base64(tar.gz) to stdout.
# import: decodes /tmp/daede-import.b64, validates, restores, restarts backend.
set -eu set -eu
umask 077
ACTION="${1:-}" ACTION="${1:-}"
LOG="/tmp/luci-app-daede.backup.log" UPLOAD="${2:-/tmp/daede-import.b64}"
IMPORT_B64="/tmp/daede-import.b64" LOG=/tmp/luci-app-daede.backup.log
MAX_TAR=184320 # 180 KiB tar.gz -> ~240 KiB base64, under the ubus limit LOCK=/tmp/luci-app-daede.config.lock
SHARE=/usr/share/luci-app-daede
# relative paths (no leading /) so tar entries match the extraction whitelist MAX_TAR=184320 # Base64 must fit inside the ubus reply (~240 KiB).
WHITELIST="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db" 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"; } 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 paths() {
export) printf '%s\n' $FILES
exist="" for sub in "$1"/etc/dae/subscriptions/*.sub; do
for p in $WHITELIST; do [ -e "/$p" ] && exist="$exist $p"; done [ -e "$sub" ] || [ -L "$sub" ] || continue
[ -n "$exist" ] || { echo "no config to back up" >&2; exit 1; } name="${sub##*/}"
tmp="$(mktemp)" case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'unexpected subscription filename' ;; esac
( cd / && tar -czf "$tmp" $exist ) 2>/dev/null || { rm -f "$tmp"; echo "tar failed" >&2; exit 1; } printf 'etc/dae/subscriptions/%s\n' "$name"
if [ "$(wc -c < "$tmp")" -gt "$MAX_TAR" ]; then done
rm -f "$tmp"; echo "config too large; use System Backup instead" >&2; exit 2 }
fi check_paths() {
b64enc "$tmp" for dir in /etc/config /etc/dae /etc/daed /etc/dae/subscriptions; do
rm -f "$tmp" [ ! -L "$dir" ] || fail "refusing symlink: $dir"
;; done
paths / > "$WORK/paths"
import) while IFS= read -r p; do
[ -f "$IMPORT_B64" ] || { echo "no upload found" >&2; exit 1; } [ ! -L "/$p" ] || fail "refusing symlink: /$p"
( [ ! -e "/$p" ] || [ -f "/$p" ] || fail "not a regular file: /$p"
exec >"$LOG" 2>&1 done < "$WORK/paths"
echo "$(date '+%F %T') begin import" }
rc=0 snapshot() {
tmp="$(mktemp)" mkdir -p "$WORK/before"
if ! b64dec "$IMPORT_B64" "$tmp"; then while IFS= read -r p; do
echo "decode failed"; rc=1 [ -f "/$p" ] || continue
elif ! gzip -t "$tmp" 2>/dev/null; then mkdir -p "$WORK/before/${p%/*}"
echo "not a valid backup archive"; rc=1 cp -p "/$p" "$WORK/before/$p"
else done < "$WORK/paths"
# reject any entry outside the whitelist (path-traversal guard) SNAPSHOT=1
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 stop_backends() {
echo "rejected: unexpected entry '$bad'"; rc=1 for svc in dae daed; do
else [ -x "/etc/init.d/$svc" ] || continue
ab="$(uci -q get daede.config.active_backend || echo dae)" if /etc/init.d/"$svc" running >/dev/null 2>&1; then RESTART="$RESTART $svc"; fi
[ -x "/etc/init.d/$ab" ] && /etc/init.d/"$ab" stop 2>/dev/null || true /etc/init.d/"$svc" stop >/dev/null 2>&1 || fail "failed to stop $svc; configuration unchanged"
if ( cd / && tar -xzf "$tmp" ); then done
echo "restored config" # procd termination can be asynchronous. Never copy a live SQLite database.
else for attempt in 1 2 3 4 5; do
echo "extract failed"; rc=1 if ! pidof dae daed daed-guard >/dev/null 2>&1; then return; fi
fi sleep 1
ab="$(uci -q get daede.config.active_backend || echo "$ab")" done
[ -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 fail 'backend still running; configuration unchanged'
fi }
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 fi
rm -f "$tmp" "$IMPORT_B64" if [ -x "/etc/init.d/$svc" ]; then
if [ "$rc" = 0 ]; then echo "result: config restored, backend restarted"; else echo "result: import failed"; fi /etc/init.d/"$svc" disable >/dev/null 2>&1 || return 1
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi /etc/init.d/"$svc" stop >/dev/null 2>&1 || return 1
) </dev/null >/dev/null 2>&1 & fi
echo "started in background, see $LOG" 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
}
*) case "$ACTION" in export|import|reset) ;; *) fail "usage: $0 export|import [upload]|reset" ;; esac
echo "usage: $0 export|import" >&2 # An atomic lock covers the complete operation, including background execution.
exit 64 mkdir "$LOCK" 2>/dev/null || fail 'another configuration operation is running'
;; WORK="$(mktemp -d /tmp/daede-config.XXXXXX)" || { rmdir "$LOCK"; exit 1; }
esac 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 daed": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "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/refresh-index.sh": [ "exec" ],
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ], "/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ]
"/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ]
}, },
"ubus": { "ubus": {
"service": [ "list" ] "service": [ "list" ]
@@ -83,7 +82,11 @@
"/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ], "/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-update.sh": [ "exec" ],
"/usr/share/luci-app-daede/daed-sub-cron.sh enable": [ "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" ] "uci": [ "dae", "daed", "daede" ]
} }
@@ -3,8 +3,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI app for FileBrowser LUCI_TITLE:=LuCI app for FileBrowser Quantum
LUCI_DEPENDS:=+filebrowser LUCI_DEPENDS:=+filebrowser-q
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
@@ -28,8 +28,8 @@ function renderStatus(isRunning, port) {
return view.extend({ return view.extend({
load: async function () { load: async function () {
const promises = await Promise.all([ const promises = await Promise.all([
L.resolveDefault(fs.stat('/var/run/filebrowser.pid'), null), L.resolveDefault(fs.stat('/var/run/filebrowser-q.pid'), null),
uci.load('filebrowser') uci.load('filebrowser-q')
]); ]);
const data = { const data = {
isRunning: promises[0], isRunning: promises[0],
@@ -42,15 +42,15 @@ return view.extend({
let m, s, o; let m, s, o;
let webport = (uci.get(data.conf, 'config', 'listen_port') || '8989'); let webport = (uci.get(data.conf, 'config', 'listen_port') || '8989');
m = new form.Map('filebrowser', _('FileBrowser'), m = new form.Map('filebrowser-q', _('FileBrowser Quantum'),
_('FileBrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files..') + '<br />'+ _('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>')); _('Default login username is %s and password is %s.').format('<code>admin</code>', '<code>admin</code>'));
s = m.section(form.TypedSection); s = m.section(form.TypedSection);
s.anonymous = true; s.anonymous = true;
s.render = function() { s.render = function() {
poll.add(function() { poll.add(function() {
return fs.stat('/var/run/filebrowser.pid').then(function(stat) { return fs.stat('/var/run/filebrowser-q.pid').then(function(stat) {
let view = document.getElementById('service_status'); let view = document.getElementById('service_status');
if (view) { if (view) {
view.innerHTML = renderStatus(stat, webport); view.innerHTML = renderStatus(stat, webport);
@@ -76,7 +76,7 @@ return view.extend({
o = s.option(form.Value, 'listen_port', _('Listen port')); o = s.option(form.Value, 'listen_port', _('Listen port'));
o.datatype = 'port'; o.datatype = 'port';
o.default = '8989'; o.default = '8787';
o.rmempty = false; o.rmempty = false;
o = s.option(form.Value, 'root_path', _('Root directory')); o = s.option(form.Value, 'root_path', _('Root directory'));
@@ -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"
}
@@ -1,55 +0,0 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:63
msgid "Collecting data..."
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:50
msgid "Default login username is %s and password is %s."
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:82
msgid "Disable Command Runner feature"
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:69
msgid "Enable"
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:31
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:33
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:48
#: applications/luci-app-filebrowser-go/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
msgid "FileBrowser"
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:49
msgid ""
"FileBrowser provides a file managing interface within a specified directory "
"and it can be used to upload, delete, preview, rename and edit your files.."
msgstr ""
#: applications/luci-app-filebrowser-go/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
msgid "Grant UCI access for luci-app-filebrowser"
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:73
msgid "Listen port"
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:33
msgid "NOT RUNNING"
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:30
msgid "Open Web Interface"
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:31
msgid "RUNNING"
msgstr ""
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:78
msgid "Root directory"
msgstr ""
-1
View File
@@ -1 +0,0 @@
zh_Hans
@@ -1,64 +0,0 @@
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"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:63
msgid "Collecting data..."
msgstr "正在收集数据中..."
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:50
msgid "Default login username is %s and password is %s."
msgstr "默认登录用户名为 %s,密码为 %s。"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:82
msgid "Disable Command Runner feature"
msgstr "禁用命令执行功能"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:69
msgid "Enable"
msgstr "启用"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:31
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:33
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:48
#: applications/luci-app-filebrowser-go/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
msgid "FileBrowser"
msgstr "FileBrowser"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:49
msgid ""
"FileBrowser provides a file managing interface within a specified directory "
"and it can be used to upload, delete, preview, rename and edit your files.."
msgstr ""
"FileBrowser 提供指定目录下的文件管理界面,可用于上传、删除、预览、重命名和编"
"辑文件。"
#: applications/luci-app-filebrowser-go/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
msgid "Grant UCI access for luci-app-filebrowser"
msgstr "授予 luci-app-filebrowser 访问 UCI 配置的权限"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:73
msgid "Listen port"
msgstr "监听端口"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:33
msgid "NOT RUNNING"
msgstr "未运行"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:30
msgid "Open Web Interface"
msgstr "打开 Web 界面"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:31
msgid "RUNNING"
msgstr "运行中"
#: applications/luci-app-filebrowser-go/htdocs/luci-static/resources/view/filebrowser.js:78
msgid "Root directory"
msgstr "根目录"
@@ -1,14 +0,0 @@
{
"admin/services/filebrowser": {
"title": "FileBrowser",
"action": {
"order": 30,
"type": "view",
"path": "filebrowser"
},
"depends": {
"acl": [ "luci-app-filebrowser" ],
"uci": { "filebrowser": true }
}
}
}
@@ -1,14 +0,0 @@
{
"luci-app-filebrowser": {
"description": "Grant UCI access for luci-app-filebrowser",
"read": {
"ubus": {
"service": [ "list" ]
},
"uci": [ "filebrowser" ]
},
"write": {
"uci": [ "filebrowser" ]
}
}
}
@@ -1,4 +0,0 @@
{
"config": "filebrowser",
"init": "filebrowser"
}
@@ -272,9 +272,17 @@ o = s:option(ListValue, "remote_dns_protocol", translate("Remote DNS Protocol"))
o:value("tcp", "TCP") o:value("tcp", "TCP")
o:value("doh", "DoH") o:value("doh", "DoH")
o:value("udp", "UDP") o:value("udp", "UDP")
o:value("tls", "TLS(DoT)", { _is_singbox = "1" }) if m.is_js_luci then
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" }) if current_node.type == "sing-box" then
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" }) o:value("tls", "TLS(DoT)")
o:value("quic", "QUIC(DoQ)")
o:value("http3", "HTTP3(DoH3)")
end
else
o:value("tls", "TLS(DoT)", { _is_singbox = "1" })
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" })
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" })
end
o:depends("_show_dns_option", "1") o:depends("_show_dns_option", "1")
---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ) ---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ)
@@ -382,9 +390,8 @@ for k, v in pairs(nodes_table) do
end end
end end
--m:appendTemplate("/acl/options", {section = arg[1]}) --[[
-- Shunt
-- Shunt Start
if current_node.protocol == "_shunt" then if current_node.protocol == "_shunt" then
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua") local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s, { setfenv(shunt_lua, getfenv(1))(m, s, {
@@ -396,5 +403,6 @@ if current_node.protocol == "_shunt" then
end end
m:appendTemplate("/acl/shunt", { shunt_list = api.jsonc.stringify(shunt_list), section = s.section }) m:appendTemplate("/acl/shunt", { shunt_list = api.jsonc.stringify(shunt_list), section = s.section })
]]--
return api.return_map(m) return api.return_map(m)
@@ -201,9 +201,17 @@ o = s:taboption("DNS", ListValue, "remote_dns_protocol", translate("Remote DNS P
o:value("tcp", "TCP") o:value("tcp", "TCP")
o:value("doh", "DoH") o:value("doh", "DoH")
o:value("udp", "UDP") o:value("udp", "UDP")
o:value("tls", "TLS(DoT)", { _is_singbox = "1" }) if m.is_js_luci then
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" }) if current_node.type == "sing-box" then
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" }) o:value("tls", "TLS(DoT)")
o:value("quic", "QUIC(DoQ)")
o:value("http3", "HTTP3(DoH3)")
end
else
o:value("tls", "TLS(DoT)", { _is_singbox = "1" })
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" })
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" })
end
---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ) ---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ)
o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS")) o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS"))
@@ -259,11 +267,10 @@ o:value("UseIP")
o:value("UseIPv4") o:value("UseIPv4")
o:value("UseIPv6") o:value("UseIPv6")
if current_node.type == "sing-box" then o = s:taboption("DNS", Value, "remote_rewrite_ttl", translate("Remote DNS") .. " TTL")
o = s:taboption("DNS", Value, "remote_rewrite_ttl", translate("Remote DNS") .. " TTL") o.datatype = "min(1)"
o.datatype = "min(1)" o.default = "30"
o.default = "30" o:depends("_is_singbox", "1")
end
o = s:taboption("DNS", TextValue, "dns_hosts", translate("Domain Override")) o = s:taboption("DNS", TextValue, "dns_hosts", translate("Domain Override"))
o.rows = 5 o.rows = 5
@@ -1,56 +0,0 @@
<%
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>
@@ -1507,15 +1507,17 @@ table td, .table .td {
//Node list option saving logic //Node list option saving logic
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
function onChange(option, value) { function onChange(option, value, refresh) {
ajax.abortAll(); ajax.abortAll();
XHR.get('<%=api.url("save_node_list_opt")%>', { XHR.get('<%=api.url("save_node_list_opt")%>', {
option: option, option: option,
value: value value: value
}, function(x) { }, function(x) {
if (x && x.status == 200) { if (x && x.status == 200) {
document.getElementById("node_list").innerHTML = ""; if (refresh) {
loadNodeList(); document.getElementById("node_list").innerHTML = "";
loadNodeList();
}
} else { } else {
alert("<%:Error%>"); alert("<%:Error%>");
} }
@@ -1527,7 +1529,7 @@ table td, .table .td {
el.addEventListener("change", () => { el.addEventListener("change", () => {
el.blur(); el.blur();
show_node_info = el.checked ? "1" : "0"; show_node_info = el.checked ? "1" : "0";
onChange("show_node_info", show_node_info); onChange("show_node_info", show_node_info, true);
}); });
}); });
@@ -1535,32 +1537,47 @@ table td, .table .td {
el.addEventListener("change", () => { el.addEventListener("change", () => {
el.blur(); el.blur();
auto_detection_time = el.value; auto_detection_time = el.value;
onChange("auto_detection_time", auto_detection_time); onChange("auto_detection_time", auto_detection_time, true);
}); });
}); });
waitForElement('select[id*="<%=appname%>"][id*="url_test_url"]', function(el) { <% if api.is_js_luci() then -%>
el.addEventListener("change", () => { waitForElement('div[id*="cbid.<%=appname%>"][id*="url_test_url"]', function(el) {
if (el.value) { el.addEventListener("cbi-dropdown-change", () => {
onChange("url_test_url", el.value); if (el.value && (!el.new_val || el.new_val != el.value)) {
} onChange("url_test_url", el.value, false);
el.new_val = el.value;
}
});
});
<% else -%>
waitForElement('select[id*="<%=appname%>"][id*="url_test_url"]', function(el) {
el.addEventListener("change", () => {
if (el.value && (!el.new_val || el.new_val != el.value)) {
onChange("url_test_url", el.value, false);
el.new_val = el.value;
}
});
}); });
});
waitForElement('input[id*="<%=appname%>"][id*="url_test_url"]', function(el) { waitForElement('input[id*="<%=appname%>"][id*="url_test_url"]', function(el) {
el.addEventListener("change", () => { el.addEventListener("change", () => {
if (el.value) { if (el.value && (!el.new_val || el.new_val != el.value)) {
onChange("url_test_url", el.value); onChange("url_test_url", el.value, false);
} el.new_val = el.value;
}
});
}); });
}); <% end -%>
} }
const ori_cbi_d_update = cbi_d_update; <% if not api.is_js_luci() then -%>
cbi_d_update = function() { const ori_cbi_d_update = cbi_d_update;
ori_cbi_d_update(); cbi_d_update = function() {
dom_event(); ori_cbi_d_update();
}; dom_event();
};
<% end -%>
dom_event(); dom_event();
const links = document.querySelectorAll('a'); const links = document.querySelectorAll('a');
@@ -3,7 +3,7 @@
. /usr/share/passwall2/utils.sh . /usr/share/passwall2/utils.sh
LOCK_FILE=${LOCK_PATH}/${CONFIG}_lease2hosts.lock 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" HOSTS_FILE="$TMP_PATH2/dhcp-hosts"
TMP_FILE="/tmp/dhcp-hosts.tmp" TMP_FILE="/tmp/dhcp-hosts.tmp"
@@ -15,7 +15,7 @@ fi
reload_dnsmasq_pids() { reload_dnsmasq_pids() {
local pidfile pid 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 if [ -s "$pidfile" ]; then
read pid < "$pidfile" read pid < "$pidfile"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196 PKG_VERSION:=196
PKG_RELEASE:=7 PKG_RELEASE:=8
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
Submodule luci-app-syncthing added at b7a197a93c
-22
View File
@@ -1,22 +0,0 @@
# Copyright (C) 2020 Gyj1109
# 适配 OpenWrt 25.12 修改版
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-syncthing
PKG_VERSION:=1.0
PKG_RELEASE:=3
LUCI_TITLE:=LuCI support for Syncthing
LUCI_PKGARCH:=all
# 适配 25.12:确保安装了 syncthing 后,LuCI 界面能正确拉起
LUCI_DEPENDS:=+syncthing
include $(TOPDIR)/feeds/luci/luci.mk
# 明确定义配置文件的归属,这对 25.12 的 apk 管理器非常重要
define Package/$(PKG_NAME)/conffiles
/etc/config/syncthing
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
-2
View File
@@ -1,2 +0,0 @@
# luci-app-syncthing
在Potat0000源码基础上进行了汉化优化,官方openwrt23.05.3编译后可使用
@@ -1,17 +0,0 @@
module("luci.controller.syncthing", package.seeall)
function index()
if not nixio.fs.access("/etc/config/syncthing") then
return
end
entry({"admin", "services", "syncthing"}, cbi("syncthing"), _("文件同步"), 10).dependent = true
entry({"admin", "services", "syncthing", "status"}, call("act_status")).leaf = true
end
function act_status()
local e = {}
e.running = luci.sys.call("pgrep syncthing >/dev/null") == 0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
@@ -1,48 +0,0 @@
require("nixio.fs")
m = Map("syncthing", translate("Syncthing同步工具"))
m:section(SimpleSection).template = "syncthing/syncthing_status"
s = m:section(TypedSection, "syncthing")
s.anonymous = true
o = s:option(Flag, "enabled", translate("启用"))
o.default = 0
o.rmempty = false
gui_address = s:option(Value, "gui_address", translate("GUI访问地址"))
gui_address.description = translate("使用0.0.0.0以监控所有访问。")
gui_address.default = "http://0.0.0.0:8384"
gui_address.placeholder = "http://0.0.0.0:8384"
gui_address.rmempty = false
home = s:option(Value, "home", translate("配置文件目录"))
home.description = translate("只有保存在/etc/syncthing中的配置会自动备份!")
home.default = "/etc/syncthing"
home.placeholder = "/etc/syncthing"
home.rmempty = false
user = s:option(ListValue, "user", translate("用户"))
user.description = translate("默认是syncthing,但这可能会导致权限被拒绝。Syncthing官方不建议以root身份运行。")
user:value("", translate("syncthing"))
for u in luci.util.execi("cat /etc/passwd | cut -d ':' -f1") do
user:value(u)
end
macprocs = s:option(Value, "macprocs", translate("线程限制"))
macprocs.description = translate("0表示匹配CPU数量(默认),>0表示显式指定并发数。")
macprocs.default = "0"
macprocs.placeholder = "0"
macprocs.datatype = "range(0,32)"
macprocs.rmempty = false
nice = s:option(Value, "nice", translate("优先级"))
nice.description = translate("显式指定优先级值。0是最高,19是最低。(暂时不允许设置负值)")
nice.default = "19"
nice.placeholder = "19"
nice.datatype = "range(0,19)"
nice.rmempty = false
return m
@@ -1,27 +0,0 @@
<script type="text/javascript">//<![CDATA[
XHR.poll(1, '<%=url([[admin]], [[services]], [[syncthing]], [[status]])%>', null,
function (x, data) {
var tb = document.getElementById('syncthing_status');
if (data && tb) {
if (data.running) {
var links = '<em><b><font color="green">Syncthing <%:运行中%></font></b></em><input class="btn cbi-button mar-10" type="button" value="<%:打开Syncthing页面%>" onclick="openwebui();" />';
tb.innerHTML = links;
} else {
tb.innerHTML = '<em><b><font color="red">Syncthing <%:未运行%></font></b></em>';
}
}
}
);
function openwebui(){
var url = window.location.host+":<%=luci.sys.exec("uci -q get syncthing.syncthing.gui_address"):match(":[0-9]+"):gsub(":", "")%>";
window.open('http://'+url,'target','');
}
//]]>
</script>
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
<fieldset class="cbi-section">
<p id="syncthing_status">
<em><%:正在收集数据...%></em>
</p>
</fieldset>
@@ -1,12 +0,0 @@
#!/bin/sh
touch /etc/config/syncthing
uci -q batch <<-EOF >/dev/null
delete ucitrack.@syncthing[-1]
add ucitrack syncthing
set ucitrack.@syncthing[-1].exec='/etc/init.d/syncthing stop && /etc/init.d/syncthing start'
commit ucitrack
EOF
# remove LuCI cache
rm -f /tmp/luci*
exit 0
@@ -1,11 +0,0 @@
{
"luci-app-syncthing": {
"description": "Grant UCI access for luci-app-syncthing",
"read": {
"uci": [ "syncthing" ]
},
"write": {
"uci": [ "syncthing" ]
}
}
}
@@ -1767,6 +1767,9 @@ static int __init fast_classifier_init(void)
printk(KERN_ALERT "fast-classifier: starting up\n"); printk(KERN_ALERT "fast-classifier: starting up\n");
DEBUG_INFO("SFE CM init\n"); DEBUG_INFO("SFE CM init\n");
/* Initialize state before registering callbacks that can use it. */
spin_lock_init(&sc->lock);
hash_init(fc_conn_ht); hash_init(fc_conn_ht);
/* /*
@@ -1882,8 +1885,6 @@ static int __init fast_classifier_init(void)
printk(KERN_ALERT "fast-classifier: registered\n"); printk(KERN_ALERT "fast-classifier: registered\n");
spin_lock_init(&sc->lock);
/* /*
* Hook the receive path in the network stack. * Hook the receive path in the network stack.
*/ */
@@ -2005,4 +2006,3 @@ module_exit(fast_classifier_exit)
MODULE_DESCRIPTION("Shortcut Forwarding Engine - Connection Manager"); MODULE_DESCRIPTION("Shortcut Forwarding Engine - Connection Manager");
MODULE_LICENSE("Dual BSD/GPL"); MODULE_LICENSE("Dual BSD/GPL");