dsh-dl-server 0.1.0 → 0.2.0
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/README.md +106 -18
- package/README.zh.md +106 -16
- package/lib/domain.js +4 -0
- package/lib/mirrors.js +102 -0
- package/lib/queue.js +800 -591
- package/lib/tools.js +3 -2
- package/lib/transport.js +68 -4
- package/package.json +15 -6
package/lib/tools.js
CHANGED
|
@@ -111,14 +111,15 @@ export function registerTools({ ctx, queue, config }) {
|
|
|
111
111
|
additionalProperties: false,
|
|
112
112
|
},
|
|
113
113
|
async execute(args) {
|
|
114
|
+
const health = queue.health()
|
|
114
115
|
if (args?.id) {
|
|
115
116
|
const job = queue.get(args.id)
|
|
116
117
|
return job
|
|
117
|
-
? { ok: true, count: 1, jobs: [job] }
|
|
118
|
+
? { ok: true, count: 1, jobs: [job], ...health }
|
|
118
119
|
: { ok: false, code: 'not-found', message: `${args.id} does not exist` }
|
|
119
120
|
}
|
|
120
121
|
const jobs = queue.list()
|
|
121
|
-
return { ok: true, count: jobs.length, jobs }
|
|
122
|
+
return { ok: true, count: jobs.length, jobs, ...health }
|
|
122
123
|
},
|
|
123
124
|
})
|
|
124
125
|
|
package/lib/transport.js
CHANGED
|
@@ -16,10 +16,13 @@ import { randomBytes } from 'node:crypto'
|
|
|
16
16
|
* @typedef {object} TransferOptions
|
|
17
17
|
* @property {string[]} urls candidate sources, tried in order
|
|
18
18
|
* @property {string} destPath absolute final file path
|
|
19
|
-
* @property {number} [connections] aria2 split, default 16
|
|
19
|
+
* @property {number} [connections] aria2 split for a large file, default 16
|
|
20
|
+
* @property {number} [connectionsCap] upper bound when the size picks the count
|
|
21
|
+
* @property {boolean} [preferDirect] skip aria2 for this job
|
|
20
22
|
* @property {number} [speedLimitKbps] 0 or absent means unlimited
|
|
21
23
|
* @property {string} [proxy] e.g. http://127.0.0.1:65532
|
|
22
24
|
* @property {number} [bytesDone] resume hint for the direct channel
|
|
25
|
+
* @property {(url: string, ok: boolean) => void} [onUrlResult]
|
|
23
26
|
* @property {(done: number, total: number) => void} [onProgress]
|
|
24
27
|
* @property {AbortSignal} [signal]
|
|
25
28
|
*
|
|
@@ -28,6 +31,28 @@ import { randomBytes } from 'node:crypto'
|
|
|
28
31
|
* @property {number} total -1 when the server never reported one
|
|
29
32
|
*/
|
|
30
33
|
|
|
34
|
+
/** Below this, aria2's setup cost exceeds any segmentation gain. */
|
|
35
|
+
export const SMALL_FILE_BYTES = 1 * 1024 * 1024
|
|
36
|
+
/** From this size up, extra connections pay for themselves. */
|
|
37
|
+
export const LARGE_FILE_BYTES = 8 * 1024 * 1024
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decide the channel and the number of connections from the file size.
|
|
41
|
+
*
|
|
42
|
+
* Segmentation only helps once `min-split-size=1M` can produce several pieces,
|
|
43
|
+
* and too many connections on a per-IP-throttled host hurts rather than helps.
|
|
44
|
+
* @param {number} sizeBytes
|
|
45
|
+
* @param {number} [cap]
|
|
46
|
+
* @returns {{ connections: number, direct: boolean }}
|
|
47
|
+
*/
|
|
48
|
+
export function planTransfer(sizeBytes, cap = 16) {
|
|
49
|
+
if (!Number.isFinite(sizeBytes) || sizeBytes <= 0) return { connections: Math.min(cap, 8), direct: false }
|
|
50
|
+
if (sizeBytes < SMALL_FILE_BYTES) return { connections: 1, direct: true }
|
|
51
|
+
if (sizeBytes < LARGE_FILE_BYTES) return { connections: Math.min(4, cap), direct: false }
|
|
52
|
+
return { connections: cap, direct: false }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
31
56
|
const IS_WIN = process.platform === 'win32'
|
|
32
57
|
const ARIA2_BIN = IS_WIN ? 'aria2c.exe' : 'aria2c'
|
|
33
58
|
|
|
@@ -222,9 +247,17 @@ export class Transport {
|
|
|
222
247
|
|
|
223
248
|
/** @param {TransferOptions} options @returns {Promise<TransferResult>} */
|
|
224
249
|
async transfer(options) {
|
|
225
|
-
|
|
250
|
+
const urls = options.urls
|
|
251
|
+
const declared = await this.#probeSize(urls, options.signal)
|
|
252
|
+
const plan = planTransfer(declared, options.connectionsCap ?? options.connections ?? this.config.connectionsPerJob ?? 16)
|
|
253
|
+
const connections = options.connections ?? plan.connections
|
|
254
|
+
const useAria2 = this.mode === 'aria2' && this.rpc && !options.preferDirect && !plan.direct
|
|
255
|
+
|
|
256
|
+
if (useAria2) {
|
|
226
257
|
try {
|
|
227
|
-
|
|
258
|
+
const result = await this.#transferAria2({ ...options, connections })
|
|
259
|
+
options.onUrlResult?.(urls[0], true)
|
|
260
|
+
return result
|
|
228
261
|
} catch (error) {
|
|
229
262
|
if (options.signal?.aborted) throw error
|
|
230
263
|
this.log('warn', `aria2 transfer failed, retrying single-stream: ${error.message}`)
|
|
@@ -233,6 +266,35 @@ export class Transport {
|
|
|
233
266
|
return this.#transferDirect(options)
|
|
234
267
|
}
|
|
235
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Best-effort Content-Length lookup. A failure is not an error: unknown size
|
|
271
|
+
* only means the planner falls back to its middle-of-the-road setting.
|
|
272
|
+
* @param {string[]} urls
|
|
273
|
+
* @param {AbortSignal} [signal]
|
|
274
|
+
* @returns {Promise<number>} the size, or -1 when it could not be determined
|
|
275
|
+
*/
|
|
276
|
+
async #probeSize(urls, signal) {
|
|
277
|
+
const url = urls[0]
|
|
278
|
+
if (!url) return -1
|
|
279
|
+
const controller = new AbortController()
|
|
280
|
+
const timer = setTimeout(() => controller.abort(), 5000)
|
|
281
|
+
timer.unref?.()
|
|
282
|
+
const onAbort = () => controller.abort()
|
|
283
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const response = await fetch(url, { method: 'HEAD', signal: controller.signal })
|
|
287
|
+
if (!response.ok) return -1
|
|
288
|
+
const length = Number(response.headers.get('content-length') ?? 0)
|
|
289
|
+
return Number.isFinite(length) && length > 0 ? length : -1
|
|
290
|
+
} catch {
|
|
291
|
+
return -1
|
|
292
|
+
} finally {
|
|
293
|
+
clearTimeout(timer)
|
|
294
|
+
signal?.removeEventListener('abort', onAbort)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
236
298
|
/** @param {TransferOptions} options */
|
|
237
299
|
async #transferAria2({ urls, destPath, connections, speedLimitKbps, onProgress, signal }) {
|
|
238
300
|
const rpc = this.rpc
|
|
@@ -284,7 +346,7 @@ export class Transport {
|
|
|
284
346
|
}
|
|
285
347
|
|
|
286
348
|
/** @param {TransferOptions} options */
|
|
287
|
-
async #transferDirect({ urls, destPath, bytesDone = 0, onProgress, signal }) {
|
|
349
|
+
async #transferDirect({ urls, destPath, bytesDone = 0, onProgress, onUrlResult, signal }) {
|
|
288
350
|
await fs.mkdir(path.dirname(destPath), { recursive: true })
|
|
289
351
|
const partPath = `${destPath}.part`
|
|
290
352
|
let lastError
|
|
@@ -326,10 +388,12 @@ export class Transport {
|
|
|
326
388
|
await pipeline(source, sink)
|
|
327
389
|
onProgress?.(written, total)
|
|
328
390
|
await fs.rename(partPath, destPath)
|
|
391
|
+
onUrlResult?.(url, true)
|
|
329
392
|
return { bytes: written, total }
|
|
330
393
|
} catch (error) {
|
|
331
394
|
if (error?.name === 'AbortError' || signal?.aborted) throw error
|
|
332
395
|
lastError = error
|
|
396
|
+
onUrlResult?.(url, false)
|
|
333
397
|
}
|
|
334
398
|
}
|
|
335
399
|
throw lastError ?? new Error('no source available')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dl-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "DLS — DSH Download Server: a global, session-aware, persistent download queue for DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"cordis.patch.yml",
|
|
15
15
|
"LICENSE",
|
|
16
16
|
"README.md",
|
|
17
|
-
"README.
|
|
17
|
+
"README.zh.md"
|
|
18
18
|
],
|
|
19
19
|
"keywords": [
|
|
20
20
|
"deepseek-harness",
|
|
@@ -37,18 +37,27 @@
|
|
|
37
37
|
"url": "https://github.com/Jestions-dev/dsh-download-server/issues"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": ">=20"
|
|
40
|
+
"node": ">=20",
|
|
41
|
+
"dsh": ">=0.2.1-rc.1 <0.3.0-0"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"test": "node --test",
|
|
44
45
|
"check": "node --check lib/index.js && node --check lib/domain.js && node --check lib/types.js"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"@deepseek-ai/cordis": "~4.1.0",
|
|
48
|
-
"@deepseek-ai/dsh-storage": "^0.2.2-rc.1",
|
|
49
|
-
"@deepseek-ai/dsh-storage-domain": "^0.2.2-rc.1",
|
|
50
48
|
"zod": "^4.4.3"
|
|
51
49
|
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.2.0-rc.1 <0.3.0-0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@deepseek-ai/dsh-storage-domain": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@deepseek-ai/dsh-storage-domain": "^0.2.2-rc.1"
|
|
60
|
+
},
|
|
52
61
|
"dsh": {
|
|
53
62
|
"bundle": {
|
|
54
63
|
"patch": "./cordis.patch.yml"
|