@twinsearth/dsh-2xray 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +6 -0
- package/index.js +34 -4
- package/lib/client.js +13 -2
- package/lib/config.js +3 -0
- package/lib/geodata.js +96 -0
- package/lib/schema.js +6 -0
- package/lib/supervisor.js +11 -0
- package/package.json +1 -1
- package/test/geodata.test.js +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
### 规则数据
|
|
6
|
+
|
|
7
|
+
- 新增 `geodata.autoUpdate`(默认 true):XTLS 主源 + jsDelivr 回退,下载 `geoip.dat`/`geosite.dat` 并原子替换。
|
|
8
|
+
- 首次缺失时在启动前下载;之后每 12 小时检查,更新后调用 `supervisor.restart()` 让 xray 重新加载。
|
|
9
|
+
- 小于 100KB 的响应视为错误页,不会覆盖现有文件。
|
|
10
|
+
- 设置页新增「每日更新规则数据」勾选框。
|
|
11
|
+
- 实测确认 Xray 的顶层 `geodata` 块只被解析、不执行 cron,因此由插件侧实现。
|
|
12
|
+
|
|
13
|
+
## 0.3.4 — 2026-10-29
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
|
|
17
|
+
- 侧边栏标题不再写死「全局代理」:读取 `status.routing.mode`,显示「全局代理 / 国内直连 / 白名单代理」;geodata 缺失降级时追加「已降级全局」。
|
|
18
|
+
- 状态卡增加「路由:…」一栏,并同步显示降级原因。
|
|
19
|
+
- 状态快照新增 `routing` 字段(mode / whitelist / blacklist)。
|
|
20
|
+
|
|
3
21
|
## 0.3.3 — 2026-10-29
|
|
4
22
|
|
|
5
23
|
### 健康检查与发布
|
package/README.md
CHANGED
|
@@ -334,6 +334,12 @@ v0.2.0 提供两个入口,互不替代:
|
|
|
334
334
|
|
|
335
335
|
维护者用 `tools/build-core-packages.mjs` 从官方 zip 生成平台包;`.github/workflows/core-packages.yml` 会在具备 `NPM_TOKEN` secret 时自动下载、校验并发布各平台包。
|
|
336
336
|
|
|
337
|
+
## 规则数据自动更新
|
|
338
|
+
|
|
339
|
+
设置页默认勾选「每日更新规则数据」:Host 会下载 XTLS 的 `geoip-lite.dat` / `geosite.dat`,失败时回退 jsDelivr(Loyalsoldier);文件小于 100KB 视为错误页并拒绝替换。首次缺失时在启动前下载,之后每 12 小时检查一次,更新成功后重启 xray 热加载。
|
|
340
|
+
|
|
341
|
+
> Xray 26.9.30 接受顶层 `geodata` 配置块但实测不执行其 cron(85 秒内无下载),因此更新由插件完成,不依赖该字段。
|
|
342
|
+
|
|
337
343
|
## 健康检查
|
|
338
344
|
|
|
339
345
|
- 进程内(需 DSH 会话鉴权):`GET /api/xray/healthz` → `{ "ok": true, "version": "v0.3.3", ... }`
|
package/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { installTools } from './lib/tools.js'
|
|
|
9
9
|
import { LogBuffer } from './lib/logbuf.js'
|
|
10
10
|
import { Monitor } from './lib/monitor.js'
|
|
11
11
|
import { checkForUpdate, installUpdate, ownVersion } from './lib/update.js'
|
|
12
|
+
import { geodataStatus, updateGeodata } from './lib/geodata.js'
|
|
12
13
|
|
|
13
14
|
export const name = 'dsh-2xray'
|
|
14
15
|
|
|
@@ -172,6 +173,7 @@ export function apply(ctx, rawConfig) {
|
|
|
172
173
|
vpnName: node?.id ?? snapshot.activeNode ?? null,
|
|
173
174
|
probe: monitor.snapshot(),
|
|
174
175
|
update: updateState,
|
|
176
|
+
geodata: geodataStatus(dataDir),
|
|
175
177
|
},
|
|
176
178
|
logs: page.items,
|
|
177
179
|
cursor: page.cursor,
|
|
@@ -281,10 +283,38 @@ export function apply(ctx, rawConfig) {
|
|
|
281
283
|
'dsh-2xray/xray-supervisor',
|
|
282
284
|
)
|
|
283
285
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
const needsGeodata = config.routing?.mode === 'cn-direct' || config.routing?.geositeCn || config.routing?.blockAds
|
|
287
|
+
const boot = async () => {
|
|
288
|
+
if (dataDir && config.geodata?.autoUpdate !== false) {
|
|
289
|
+
const status = geodataStatus(dataDir)
|
|
290
|
+
if (status.missing.length > 0 && needsGeodata) {
|
|
291
|
+
// Download before the first spawn, otherwise cn-direct would either
|
|
292
|
+
// fail or degrade to global on a fresh machine.
|
|
293
|
+
const result = await updateGeodata({ dataDir, logger, timeoutMs: 20_000 }).catch((error) => ({
|
|
294
|
+
updated: [],
|
|
295
|
+
failed: ['download'],
|
|
296
|
+
error,
|
|
297
|
+
}))
|
|
298
|
+
if (result?.updated?.length > 0) logger.info('规则数据已下载,使用最新 geodata 启动 xray')
|
|
299
|
+
}
|
|
300
|
+
const refresh = () => {
|
|
301
|
+
;(async () => {
|
|
302
|
+
if (!geodataStatus(dataDir).stale) return
|
|
303
|
+
const result = await updateGeodata({ dataDir, logger })
|
|
304
|
+
if (result?.updated?.length > 0) {
|
|
305
|
+
logger.info('规则数据已更新,重启 xray 加载新数据')
|
|
306
|
+
await supervisor.restart('geodata-updated')
|
|
307
|
+
}
|
|
308
|
+
})().catch((error) => logger.warn(`规则数据更新失败:${String(error)}`))
|
|
309
|
+
}
|
|
310
|
+
const first = setTimeout(refresh, 60_000)
|
|
311
|
+
const daily = setInterval(refresh, 12 * 60 * 60 * 1000)
|
|
312
|
+
first.unref?.()
|
|
313
|
+
daily.unref?.()
|
|
314
|
+
}
|
|
315
|
+
await supervisor.start()
|
|
316
|
+
}
|
|
317
|
+
boot().catch((error) => logger.error(`xray 监督器异常:${String(error)}`))
|
|
288
318
|
installTools({ ctx, supervisor, logger }).catch((error) => logger.warn(`安装 xray_* 工具失败:${String(error)}`))
|
|
289
319
|
|
|
290
320
|
return dispose
|
package/lib/client.js
CHANGED
|
@@ -199,6 +199,8 @@ window.__ModuleLoader__.load({
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
function StatusCard({ status, latency, egress, phase }) {
|
|
202
|
+
const routingLabel =
|
|
203
|
+
{ global: '全局', 'cn-direct': '国内直连', whitelist: '白名单' }[status?.routing?.mode] ?? '全局'
|
|
202
204
|
return h('section', { style: C.card },
|
|
203
205
|
h('div', { style: C.cardHead }, '③ 连接状态'),
|
|
204
206
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
|
|
@@ -209,6 +211,8 @@ window.__ModuleLoader__.load({
|
|
|
209
211
|
egress?.ip ? `出口 IP ${egress.ip}${egress.country ? ` (${egress.country})` : ''}` : '出口 IP —')),
|
|
210
212
|
h('div', { style: { ...C.muted, fontSize: 12.5, marginTop: 6 } },
|
|
211
213
|
`节点「${status?.vpnName ?? status?.activeNode ?? '—'}」` +
|
|
214
|
+
` · 路由:${routingLabel}` +
|
|
215
|
+
(status?.routingFallback ? '(geodata 缺失,已降级全局)' : '') +
|
|
212
216
|
(status?.route?.installed ? ` · ${status.route.mode === 'harness' ? 'DSH 策略已接管' : '内置 dispatcher'}` : ' · 尚未接管')))
|
|
213
217
|
}
|
|
214
218
|
|
|
@@ -238,13 +242,16 @@ window.__ModuleLoader__.load({
|
|
|
238
242
|
const latency = probe.latency?.ms
|
|
239
243
|
const egress = probe.egress ?? {}
|
|
240
244
|
const localEndpoint = status ? `127.0.0.1:${status.port}` : '—'
|
|
245
|
+
const routingLabel =
|
|
246
|
+
{ global: '全局代理', 'cn-direct': '国内直连', whitelist: '白名单代理' }[status?.routing?.mode] ?? '全局代理'
|
|
247
|
+
const fallbackNote = status?.routingFallback ? ' · geodata 缺失,已降级全局' : ''
|
|
241
248
|
|
|
242
249
|
return h('div', { style: C.page },
|
|
243
250
|
h('div', { style: C.header },
|
|
244
251
|
h('span', { style: C.globe }, h(GlobeIcon, { size: 22 })),
|
|
245
252
|
h('div', null,
|
|
246
253
|
h('h1', { style: C.title }, '2Xray'),
|
|
247
|
-
h('p', { style: C.sub }, `Xray
|
|
254
|
+
h('p', { style: C.sub }, `Xray ${routingLabel} · 本地 ${localEndpoint}${fallbackNote}`)),
|
|
248
255
|
h(StatusPill, { status })),
|
|
249
256
|
error
|
|
250
257
|
? h('div', { style: { ...C.card, ...C.error, fontSize: 12.5 } }, `状态通道不可用:${error}(代理本身可能仍在工作)`)
|
|
@@ -319,6 +326,7 @@ window.__ModuleLoader__.load({
|
|
|
319
326
|
const [autoUpdate, setAutoUpdate] = React.useState(value.updates?.auto === true)
|
|
320
327
|
const [updateInfo, setUpdateInfo] = React.useState(null)
|
|
321
328
|
const [updateBusy, setUpdateBusy] = React.useState('')
|
|
329
|
+
const [autoGeodata, setAutoGeodata] = React.useState(value.geodata?.autoUpdate !== false)
|
|
322
330
|
const [message, setMessage] = React.useState('')
|
|
323
331
|
const [kind, setKind] = React.useState('')
|
|
324
332
|
const [busy, setBusy] = React.useState(false)
|
|
@@ -471,6 +479,7 @@ window.__ModuleLoader__.load({
|
|
|
471
479
|
{ path: ['routing', 'whitelist'], value: splitList(whitelist) },
|
|
472
480
|
{ path: ['routing', 'blacklist'], value: splitList(blacklist) },
|
|
473
481
|
{ path: ['updates', 'auto'], value: autoUpdate },
|
|
482
|
+
{ path: ['geodata', 'autoUpdate'], value: autoGeodata },
|
|
474
483
|
]
|
|
475
484
|
if (binaryPath.trim()) fields.push({ path: ['binary', 'path'], value: binaryPath.trim() })
|
|
476
485
|
const numericPort = Number(port)
|
|
@@ -542,7 +551,9 @@ window.__ModuleLoader__.load({
|
|
|
542
551
|
onClick: installNow,
|
|
543
552
|
}, updateBusy === 'install' ? '更新中…' : '立即更新'),
|
|
544
553
|
h('label', { style: { fontSize: 12.5, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
545
|
-
h('input', { type: 'checkbox', checked: autoUpdate, onChange: (event) => setAutoUpdate(event.target.checked) }), '自动更新')
|
|
554
|
+
h('input', { type: 'checkbox', checked: autoUpdate, onChange: (event) => setAutoUpdate(event.target.checked) }), '自动更新'),
|
|
555
|
+
h('label', { style: { fontSize: 12.5, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
556
|
+
h('input', { type: 'checkbox', checked: autoGeodata, onChange: (event) => setAutoGeodata(event.target.checked) }), '每日更新规则数据')),
|
|
546
557
|
h('div', { style: { ...C.muted, fontSize: 12, marginTop: 6 } },
|
|
547
558
|
updateInfo
|
|
548
559
|
? `当前 ${updateInfo.current} · 最新 ${updateInfo.latest ?? '未知'}${updateInfo.available ? '(可更新)' : '(已是最新)'}` +
|
package/lib/config.js
CHANGED
|
@@ -70,6 +70,7 @@ export const DEFAULT_CONFIG = {
|
|
|
70
70
|
timeoutMs: 3000,
|
|
71
71
|
},
|
|
72
72
|
updates: { auto: false },
|
|
73
|
+
geodata: { autoUpdate: true },
|
|
73
74
|
binary: { path: '', dataDir: '' },
|
|
74
75
|
lifecycle: {
|
|
75
76
|
startTimeoutMs: 15000,
|
|
@@ -353,6 +354,8 @@ export function normalizeConfig(raw) {
|
|
|
353
354
|
|
|
354
355
|
merged.updates = merged.updates ?? {}
|
|
355
356
|
merged.updates.auto = asBool(merged.updates.auto, false)
|
|
357
|
+
merged.geodata = merged.geodata ?? {}
|
|
358
|
+
merged.geodata.autoUpdate = asBool(merged.geodata.autoUpdate, true)
|
|
356
359
|
|
|
357
360
|
const control = merged.control
|
|
358
361
|
control.enabled = asBool(control.enabled, false)
|
package/lib/geodata.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
5
|
+
const MIN_BYTES = 100_000
|
|
6
|
+
|
|
7
|
+
export const GEODATA_FILES = [
|
|
8
|
+
{
|
|
9
|
+
file: 'geoip.dat',
|
|
10
|
+
urls: [
|
|
11
|
+
'https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geoip-lite.dat',
|
|
12
|
+
'https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat',
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
file: 'geosite.dat',
|
|
17
|
+
urls: [
|
|
18
|
+
'https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geosite.dat',
|
|
19
|
+
'https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat',
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export function geodataStatus(dataDir) {
|
|
25
|
+
if (!dataDir) return { files: [], stale: true, missing: GEODATA_FILES.map((entry) => entry.file) }
|
|
26
|
+
const files = []
|
|
27
|
+
const missing = []
|
|
28
|
+
let oldest = Infinity
|
|
29
|
+
for (const entry of GEODATA_FILES) {
|
|
30
|
+
const path = join(dataDir, entry.file)
|
|
31
|
+
try {
|
|
32
|
+
const stat = statSync(path)
|
|
33
|
+
files.push({ file: entry.file, size: stat.size, mtimeMs: stat.mtimeMs })
|
|
34
|
+
oldest = Math.min(oldest, stat.mtimeMs)
|
|
35
|
+
} catch {
|
|
36
|
+
missing.push(entry.file)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
files,
|
|
41
|
+
missing,
|
|
42
|
+
stale: missing.length > 0 || Date.now() - oldest > DAY_MS,
|
|
43
|
+
updatedAt: files.length > 0 ? Math.min(...files.map((entry) => entry.mtimeMs)) : null,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function download(url, timeoutMs) {
|
|
48
|
+
const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(timeoutMs) })
|
|
49
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
50
|
+
const bytes = Buffer.from(await response.arrayBuffer())
|
|
51
|
+
if (bytes.length < MIN_BYTES) throw new Error(`payload too small (${bytes.length} B)`)
|
|
52
|
+
return bytes
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Xray accepts a top-level `geodata` block but does not execute its cron (live
|
|
57
|
+
* test: no download within 85 s), so the host performs the update itself and
|
|
58
|
+
* restarts the child afterwards. Writes are atomic per file.
|
|
59
|
+
*/
|
|
60
|
+
export async function updateGeodata({ dataDir, logger, timeoutMs = 30_000, force = false, files = GEODATA_FILES } = {}) {
|
|
61
|
+
if (!dataDir) return { updated: [], failed: [], skipped: true }
|
|
62
|
+
mkdirSync(dataDir, { recursive: true })
|
|
63
|
+
const status = geodataStatus(dataDir)
|
|
64
|
+
if (!force && !status.stale) {
|
|
65
|
+
return { updated: [], failed: [], skipped: true, status }
|
|
66
|
+
}
|
|
67
|
+
const updated = []
|
|
68
|
+
const failed = []
|
|
69
|
+
for (const entry of files) {
|
|
70
|
+
const target = join(dataDir, entry.file)
|
|
71
|
+
let done = false
|
|
72
|
+
for (const url of entry.urls) {
|
|
73
|
+
try {
|
|
74
|
+
const bytes = await download(url, timeoutMs)
|
|
75
|
+
const temporary = `${target}.tmp-${process.pid}`
|
|
76
|
+
writeFileSync(temporary, bytes)
|
|
77
|
+
renameSync(temporary, target)
|
|
78
|
+
updated.push({ file: entry.file, bytes: bytes.length, url })
|
|
79
|
+
logger?.info?.(`规则数据已更新:${entry.file}(${(bytes.length / 1048576).toFixed(1)} MB)`)
|
|
80
|
+
done = true
|
|
81
|
+
break
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger?.warn?.(`下载 ${entry.file} 失败(${url}):${String(error.message ?? error)}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!done) {
|
|
87
|
+
failed.push(entry.file)
|
|
88
|
+
try {
|
|
89
|
+
rmSync(`${target}.tmp-${process.pid}`, { force: true })
|
|
90
|
+
} catch {
|
|
91
|
+
// ignore
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { updated, failed, status: geodataStatus(dataDir) }
|
|
96
|
+
}
|
package/lib/schema.js
CHANGED
|
@@ -113,4 +113,10 @@ export const Config = Schema.object({
|
|
|
113
113
|
.description('启动时检查 npm 上的新版本并自动安装;重启 DSH 后生效。')
|
|
114
114
|
.volatile(),
|
|
115
115
|
}),
|
|
116
|
+
geodata: Schema.object({
|
|
117
|
+
autoUpdate: Schema.boolean()
|
|
118
|
+
.default(true)
|
|
119
|
+
.description('每日自动下载 geoip.dat/geosite.dat(XTLS 主源,jsDelivr 回退),更新后重启 xray。')
|
|
120
|
+
.volatile(),
|
|
121
|
+
}),
|
|
116
122
|
})
|
package/lib/supervisor.js
CHANGED
|
@@ -165,6 +165,12 @@ export class Supervisor {
|
|
|
165
165
|
return this.#startTask
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/** Restart the child so a freshly downloaded geodata set is loaded. */
|
|
169
|
+
async restart(reason = 'manual') {
|
|
170
|
+
if (this.#stopped || this.#stopRequested) return
|
|
171
|
+
await this.#recycle(reason)
|
|
172
|
+
}
|
|
173
|
+
|
|
168
174
|
async stop() {
|
|
169
175
|
if (this.#stopped) return
|
|
170
176
|
this.#stopped = true
|
|
@@ -732,6 +738,11 @@ export class Supervisor {
|
|
|
732
738
|
takeover: this.config.takeover,
|
|
733
739
|
settingsSchema: this.#schemaLoaded,
|
|
734
740
|
routingFallback: this.#routingFallback,
|
|
741
|
+
routing: {
|
|
742
|
+
mode: this.config.routing?.mode ?? 'global',
|
|
743
|
+
whitelist: this.config.routing?.whitelist ?? [],
|
|
744
|
+
blacklist: this.config.routing?.blacklist ?? [],
|
|
745
|
+
},
|
|
735
746
|
nodes: this.config.nodes.map((node) => ({
|
|
736
747
|
id: node.id,
|
|
737
748
|
tag: node.tag,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/dsh-2xray",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "DSH-2Xray: supervises xray-core and routes DSH process-wide HTTP(S) traffic through its local inbound, with optional multi-node gRPC control plane",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import http from 'node:http'
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { geodataStatus, updateGeodata } from '../lib/geodata.js'
|
|
8
|
+
|
|
9
|
+
async function withServer(payload, run) {
|
|
10
|
+
const server = http.createServer((request, response) => {
|
|
11
|
+
response.writeHead(200, { 'content-type': 'application/octet-stream' })
|
|
12
|
+
response.end(payload)
|
|
13
|
+
})
|
|
14
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
15
|
+
const { port } = server.address()
|
|
16
|
+
try {
|
|
17
|
+
return await run(`http://127.0.0.1:${port}`)
|
|
18
|
+
} finally {
|
|
19
|
+
server.close()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('downloads a geodata file atomically and reports freshness', async () => {
|
|
24
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-geodata-'))
|
|
25
|
+
try {
|
|
26
|
+
const payload = Buffer.alloc(200_000, 0x1f)
|
|
27
|
+
await withServer(payload, async (base) => {
|
|
28
|
+
const result = await updateGeodata({
|
|
29
|
+
dataDir: dir,
|
|
30
|
+
force: true,
|
|
31
|
+
files: [{ file: 'geoip.dat', urls: [`${base}/geoip.dat`] }],
|
|
32
|
+
})
|
|
33
|
+
assert.deepEqual(result.updated.map((entry) => entry.file), ['geoip.dat'])
|
|
34
|
+
assert.equal(existsSync(join(dir, 'geoip.dat')), true)
|
|
35
|
+
assert.equal(statSync(join(dir, 'geoip.dat')).size, payload.length)
|
|
36
|
+
assert.deepEqual(geodataStatus(dir).missing, ['geosite.dat'])
|
|
37
|
+
})
|
|
38
|
+
} finally {
|
|
39
|
+
rmSync(dir, { recursive: true, force: true })
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('a tiny payload is rejected instead of replacing the file', async () => {
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-geodata-'))
|
|
45
|
+
try {
|
|
46
|
+
await withServer(Buffer.from('<html>blocked</html>'), async (base) => {
|
|
47
|
+
const result = await updateGeodata({
|
|
48
|
+
dataDir: dir,
|
|
49
|
+
force: true,
|
|
50
|
+
files: [{ file: 'geosite.dat', urls: [`${base}/geosite.dat`] }],
|
|
51
|
+
})
|
|
52
|
+
assert.deepEqual(result.updated, [])
|
|
53
|
+
assert.deepEqual(result.failed, ['geosite.dat'])
|
|
54
|
+
assert.equal(existsSync(join(dir, 'geosite.dat')), false)
|
|
55
|
+
})
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(dir, { recursive: true, force: true })
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('fresh files are skipped unless forced', async () => {
|
|
62
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-geodata-'))
|
|
63
|
+
try {
|
|
64
|
+
const payload = Buffer.alloc(200_000, 0x1f)
|
|
65
|
+
await withServer(payload, async (base) => {
|
|
66
|
+
const files = [
|
|
67
|
+
{ file: 'geoip.dat', urls: [`${base}/geoip.dat`] },
|
|
68
|
+
{ file: 'geosite.dat', urls: [`${base}/geosite.dat`] },
|
|
69
|
+
]
|
|
70
|
+
await updateGeodata({ dataDir: dir, force: true, files })
|
|
71
|
+
const second = await updateGeodata({ dataDir: dir, files })
|
|
72
|
+
assert.equal(second.skipped, true)
|
|
73
|
+
assert.equal(readFileSync(join(dir, 'geosite.dat')).length, payload.length)
|
|
74
|
+
})
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(dir, { recursive: true, force: true })
|
|
77
|
+
}
|
|
78
|
+
})
|