@twinsearth/dsh-2xray 0.3.4 → 0.3.6
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 +19 -0
- package/README.md +16 -0
- package/index.js +34 -4
- package/lib/client.js +5 -1
- package/lib/config.js +3 -0
- package/lib/geodata.js +96 -0
- package/lib/schema.js +6 -0
- package/lib/supervisor.js +6 -0
- package/package.json +1 -1
- package/test/geodata.test.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.6 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
### 规则数据
|
|
6
|
+
|
|
7
|
+
- 自动更新周期从 24h/12h 调整为 **72 小时**:新鲜度阈值与定时检查间隔一致。
|
|
8
|
+
- 启动前仍会补缺(缺失时先下载再启动);更新成功后重启 xray 加载新数据。
|
|
9
|
+
- 小于 100KB 的响应仍视为错误页,不覆盖现有文件。
|
|
10
|
+
- 设置页文案改为「每 72 小时更新规则数据」。
|
|
11
|
+
|
|
12
|
+
## 0.3.5 — 2026-10-29
|
|
13
|
+
|
|
14
|
+
### 规则数据
|
|
15
|
+
|
|
16
|
+
- 新增 `geodata.autoUpdate`(默认 true):XTLS 主源 + jsDelivr 回退,下载 `geoip.dat`/`geosite.dat` 并原子替换。
|
|
17
|
+
- 首次缺失时在启动前下载;之后每 12 小时检查,更新后调用 `supervisor.restart()` 让 xray 重新加载。
|
|
18
|
+
- 小于 100KB 的响应视为错误页,不会覆盖现有文件。
|
|
19
|
+
- 设置页新增「每日更新规则数据」勾选框。
|
|
20
|
+
- 实测确认 Xray 的顶层 `geodata` 块只被解析、不执行 cron,因此由插件侧实现。
|
|
21
|
+
|
|
3
22
|
## 0.3.4 — 2026-10-29
|
|
4
23
|
|
|
5
24
|
### 修复
|
package/README.md
CHANGED
|
@@ -334,11 +334,27 @@ 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
|
+
设置页默认勾选「每 72 小时更新规则数据」:Host 会下载 XTLS 的 `geoip-lite.dat` / `geosite.dat`,失败时回退 jsDelivr(Loyalsoldier);文件小于 100KB 视为错误页并拒绝替换。首次缺失时在启动前下载,之后每 72 小时检查一次,更新成功后重启 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", ... }`
|
|
340
346
|
- 发布物(公开、静态):Release 资产 `healthz.json`,`version` 与 tag 一致,可用于部署流水线的健康探针。
|
|
341
347
|
|
|
348
|
+
## DSH 插件市场
|
|
349
|
+
|
|
350
|
+
索引地址(format 1):
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
https://raw.githubusercontent.com/TwinsEarth/DSH-2Xray/main/market/index.json
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
在 DSH 的「设置 → 插件市场 → 添加源」粘贴该地址,即可搜索并安装 DSH-2Xray。索引只声明 npm 包 `@twinsearth/dsh-2xray` 与其版本,安装仍走 npm registry。
|
|
357
|
+
|
|
342
358
|
## 更新
|
|
343
359
|
|
|
344
360
|
设置页 2Xray 里有三个入口:
|
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, 72 * 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
|
@@ -326,6 +326,7 @@ window.__ModuleLoader__.load({
|
|
|
326
326
|
const [autoUpdate, setAutoUpdate] = React.useState(value.updates?.auto === true)
|
|
327
327
|
const [updateInfo, setUpdateInfo] = React.useState(null)
|
|
328
328
|
const [updateBusy, setUpdateBusy] = React.useState('')
|
|
329
|
+
const [autoGeodata, setAutoGeodata] = React.useState(value.geodata?.autoUpdate !== false)
|
|
329
330
|
const [message, setMessage] = React.useState('')
|
|
330
331
|
const [kind, setKind] = React.useState('')
|
|
331
332
|
const [busy, setBusy] = React.useState(false)
|
|
@@ -478,6 +479,7 @@ window.__ModuleLoader__.load({
|
|
|
478
479
|
{ path: ['routing', 'whitelist'], value: splitList(whitelist) },
|
|
479
480
|
{ path: ['routing', 'blacklist'], value: splitList(blacklist) },
|
|
480
481
|
{ path: ['updates', 'auto'], value: autoUpdate },
|
|
482
|
+
{ path: ['geodata', 'autoUpdate'], value: autoGeodata },
|
|
481
483
|
]
|
|
482
484
|
if (binaryPath.trim()) fields.push({ path: ['binary', 'path'], value: binaryPath.trim() })
|
|
483
485
|
const numericPort = Number(port)
|
|
@@ -549,7 +551,9 @@ window.__ModuleLoader__.load({
|
|
|
549
551
|
onClick: installNow,
|
|
550
552
|
}, updateBusy === 'install' ? '更新中…' : '立即更新'),
|
|
551
553
|
h('label', { style: { fontSize: 12.5, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
552
|
-
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) }), '每 72 小时更新规则数据')),
|
|
553
557
|
h('div', { style: { ...C.muted, fontSize: 12, marginTop: 6 } },
|
|
554
558
|
updateInfo
|
|
555
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 REFRESH_MS = 72 * 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 > REFRESH_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('每 72 小时自动下载 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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/dsh-2xray",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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,92 @@
|
|
|
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, utimesSync, writeFileSync } 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
|
+
})
|
|
79
|
+
|
|
80
|
+
test('the freshness window is 72 hours', () => {
|
|
81
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-geodata-'))
|
|
82
|
+
try {
|
|
83
|
+
for (const file of ['geoip.dat', 'geosite.dat']) writeFileSync(join(dir, file), Buffer.alloc(200_000, 0x1f))
|
|
84
|
+
const now = Math.floor(Date.now() / 1000)
|
|
85
|
+
for (const file of ['geoip.dat', 'geosite.dat']) utimesSync(join(dir, file), now - 48 * 3600, now - 48 * 3600)
|
|
86
|
+
assert.equal(geodataStatus(dir).stale, false, '48h old must still be fresh')
|
|
87
|
+
for (const file of ['geoip.dat', 'geosite.dat']) utimesSync(join(dir, file), now - 73 * 3600, now - 73 * 3600)
|
|
88
|
+
assert.equal(geodataStatus(dir).stale, true, '73h old must be stale')
|
|
89
|
+
} finally {
|
|
90
|
+
rmSync(dir, { recursive: true, force: true })
|
|
91
|
+
}
|
|
92
|
+
})
|