@juzi/file-box 1.7.19 → 1.7.20

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.
Files changed (46) hide show
  1. package/dist/cjs/src/file-box.d.ts.map +1 -1
  2. package/dist/cjs/src/file-box.js +2 -2
  3. package/dist/cjs/src/file-box.js.map +1 -1
  4. package/dist/cjs/src/file-box.spec.d.ts.map +1 -1
  5. package/dist/cjs/src/file-box.spec.js +26 -2
  6. package/dist/cjs/src/file-box.spec.js.map +1 -1
  7. package/dist/cjs/src/misc.d.ts.map +1 -1
  8. package/dist/cjs/src/misc.js +126 -27
  9. package/dist/cjs/src/misc.js.map +1 -1
  10. package/dist/cjs/src/misc.spec.js +101 -14
  11. package/dist/cjs/src/misc.spec.js.map +1 -1
  12. package/dist/cjs/src/version.js +1 -1
  13. package/dist/cjs/tests/chunk-download.spec.d.ts +3 -0
  14. package/dist/cjs/tests/chunk-download.spec.d.ts.map +1 -0
  15. package/dist/cjs/tests/chunk-download.spec.js +302 -0
  16. package/dist/cjs/tests/chunk-download.spec.js.map +1 -0
  17. package/dist/cjs/tests/misc-error-handling.spec.d.ts +3 -0
  18. package/dist/cjs/tests/misc-error-handling.spec.d.ts.map +1 -0
  19. package/dist/cjs/tests/misc-error-handling.spec.js +258 -0
  20. package/dist/cjs/tests/misc-error-handling.spec.js.map +1 -0
  21. package/dist/esm/src/file-box.d.ts.map +1 -1
  22. package/dist/esm/src/file-box.js +2 -2
  23. package/dist/esm/src/file-box.js.map +1 -1
  24. package/dist/esm/src/file-box.spec.d.ts.map +1 -1
  25. package/dist/esm/src/file-box.spec.js +27 -3
  26. package/dist/esm/src/file-box.spec.js.map +1 -1
  27. package/dist/esm/src/misc.d.ts.map +1 -1
  28. package/dist/esm/src/misc.js +127 -28
  29. package/dist/esm/src/misc.js.map +1 -1
  30. package/dist/esm/src/misc.spec.js +101 -14
  31. package/dist/esm/src/misc.spec.js.map +1 -1
  32. package/dist/esm/src/version.js +1 -1
  33. package/dist/esm/tests/chunk-download.spec.d.ts +3 -0
  34. package/dist/esm/tests/chunk-download.spec.d.ts.map +1 -0
  35. package/dist/esm/tests/chunk-download.spec.js +300 -0
  36. package/dist/esm/tests/chunk-download.spec.js.map +1 -0
  37. package/dist/esm/tests/misc-error-handling.spec.d.ts +3 -0
  38. package/dist/esm/tests/misc-error-handling.spec.d.ts.map +1 -0
  39. package/dist/esm/tests/misc-error-handling.spec.js +256 -0
  40. package/dist/esm/tests/misc-error-handling.spec.js.map +1 -0
  41. package/package.json +10 -3
  42. package/src/file-box.spec.ts +34 -7
  43. package/src/file-box.ts +5 -5
  44. package/src/misc.spec.ts +111 -17
  45. package/src/misc.ts +110 -31
  46. package/src/version.ts +1 -1
@@ -2,13 +2,15 @@
2
2
 
3
3
  import 'reflect-metadata'
4
4
 
5
- import assert from 'assert'
6
- import { PassThrough, Readable } from 'stream'
7
- import { test, sinon } from 'tstest'
8
- import { instanceToClass } from 'clone-class'
5
+ import assert from 'assert'
6
+ import { instanceToClass } from 'clone-class'
7
+ import { createServer } from 'http'
8
+ import type { AddressInfo } from 'net'
9
+ import { PassThrough, Readable } from 'stream'
10
+ import { sinon, test } from 'tstest'
9
11
 
10
- import { FileBox } from './file-box.js'
11
- import { FileBoxType } from './file-box.type.js'
12
+ import { FileBox } from './file-box.js'
13
+ import { FileBoxType } from './file-box.type.js'
12
14
 
13
15
  const requiredMetadataKey = Symbol('required')
14
16
 
@@ -113,7 +115,32 @@ test('syncRemote()', async t => {
113
115
 
114
116
  }
115
117
 
116
- const URL = 'http://httpbin.org/response-headers?Content-Disposition=attachment;%20filename%3d%22test.txt%22&filename=test.txt'
118
+ /**
119
+ * 使用本地 server,避免依赖外网(httpbin 不稳定/可能被墙/会返回 html)
120
+ */
121
+ const server = createServer((req, res) => {
122
+ if (req.method === 'HEAD' && req.url?.startsWith('/response-headers')) {
123
+ res.writeHead(200, {
124
+ 'Content-Disposition': 'attachment; filename="test.txt"',
125
+ 'Content-Length': '159',
126
+ 'Content-Type': 'application/json',
127
+ })
128
+ res.end()
129
+ return
130
+ }
131
+ res.writeHead(404)
132
+ res.end()
133
+ })
134
+
135
+ const host = await new Promise<string>((resolve) => {
136
+ server.listen(0, '127.0.0.1', () => {
137
+ const addr = server.address() as AddressInfo
138
+ resolve(`http://127.0.0.1:${addr.port}`)
139
+ })
140
+ })
141
+ t.teardown(() => { server.close() })
142
+
143
+ const URL = `${host}/response-headers?Content-Disposition=attachment;%20filename%3d%22test.txt%22&filename=test.txt`
117
144
 
118
145
  const EXPECTED_NAME_FROM_URL = 'response-headers'
119
146
  const EXPECTED_TYPE_FROM_URL = 'application/unknown'
package/src/file-box.ts CHANGED
@@ -385,10 +385,10 @@ class FileBox implements Pipeable, FileBoxInterface {
385
385
 
386
386
  case FileBoxType.Url:
387
387
  fileBox = this.fromUrl(obj.url, {
388
+ headers: obj.headers,
388
389
  md5: obj.md5,
389
390
  name: obj.name,
390
391
  size: obj.size,
391
- headers: obj.headers,
392
392
  })
393
393
  break
394
394
 
@@ -510,12 +510,12 @@ class FileBox implements Pipeable, FileBoxInterface {
510
510
  private readonly qrCode? : string
511
511
  private readonly uuid? : string
512
512
 
513
- get url() {
513
+ get url () {
514
514
  return this.remoteUrl
515
515
  }
516
516
 
517
517
  private proxyUrl?: string
518
- useProxyUrl(url?: string) {
518
+ useProxyUrl (url?: string) {
519
519
  this.proxyUrl = url
520
520
  }
521
521
 
@@ -618,7 +618,7 @@ class FileBox implements Pipeable, FileBoxInterface {
618
618
 
619
619
  async ready (): Promise<void> {
620
620
  let tryCount = 0
621
- while (1) {
621
+ while (true) {
622
622
  try {
623
623
  switch (this.type) {
624
624
  case FileBoxType.Url:
@@ -636,7 +636,7 @@ class FileBox implements Pipeable, FileBoxInterface {
636
636
  }
637
637
  break
638
638
  } catch (e) {
639
- tryCount ++
639
+ tryCount++
640
640
  if (tryCount >= READY_RETRY) {
641
641
  throw e
642
642
  }
package/src/misc.spec.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
2
 
3
3
  // tslint:disable:no-shadowed-variable
4
+ import { createServer } from 'http'
5
+ import type { AddressInfo } from 'net'
4
6
  import { test } from 'tstest'
5
7
 
6
8
  import {
@@ -27,14 +29,42 @@ test('dataUrl to base64', async t => {
27
29
  })
28
30
 
29
31
  test('httpHeadHeader', async t => {
30
- const URL = 'https://github.com/huan/file-box/archive/v0.6.tar.gz'
31
-
32
- const EXPECTED_HEADERS_KEY = 'content-disposition'
33
- const EXPECTED_HEADERS_VALUE = 'attachment; filename=file-box-0.6.tar.gz'
34
-
35
- const headers = await httpHeadHeader(URL)
36
-
37
- t.equal(headers[EXPECTED_HEADERS_KEY], EXPECTED_HEADERS_VALUE, 'should get the headers right')
32
+ /**
33
+ * 使用本地 server,避免依赖外网(CI/本地网络不稳定会导致 flaky)
34
+ * 同时覆盖:302 Location 为相对路径的跳转逻辑
35
+ */
36
+ const server = createServer((req, res) => {
37
+ if (req.url === '/redirect') {
38
+ res.writeHead(302, { Location: '/final' })
39
+ res.end()
40
+ return
41
+ }
42
+ if (req.url === '/final') {
43
+ res.writeHead(200, {
44
+ 'Content-Disposition': 'attachment; filename=file-box-0.6.tar.gz',
45
+ 'Content-Length': '0',
46
+ })
47
+ res.end()
48
+ return
49
+ }
50
+ res.writeHead(404)
51
+ res.end()
52
+ })
53
+
54
+ const host = await new Promise<string>((resolve) => {
55
+ server.listen(0, '127.0.0.1', () => {
56
+ const addr = server.address() as AddressInfo
57
+ resolve(`http://127.0.0.1:${addr.port}`)
58
+ })
59
+ })
60
+ t.teardown(() => { server.close() })
61
+
62
+ const headers = await httpHeadHeader(`${host}/redirect`)
63
+ t.equal(
64
+ headers['content-disposition'],
65
+ 'attachment; filename=file-box-0.6.tar.gz',
66
+ 'should get the headers right',
67
+ )
38
68
  })
39
69
 
40
70
  test('httpHeaderToFileName', async t => {
@@ -54,7 +84,18 @@ test('httpHeaderToFileName', async t => {
54
84
  })
55
85
 
56
86
  test('httpStream', async t => {
57
- const URL = 'https://httpbin.org/headers'
87
+ const server = createServer((req, res) => {
88
+ res.writeHead(200, { 'Content-Type': 'application/json' })
89
+ res.end(JSON.stringify({ headers: req.headers }))
90
+ })
91
+
92
+ const host = await new Promise<string>((resolve) => {
93
+ server.listen(0, '127.0.0.1', () => {
94
+ const addr = server.address() as AddressInfo
95
+ resolve(`http://127.0.0.1:${addr.port}`)
96
+ })
97
+ })
98
+ t.teardown(() => { server.close() })
58
99
 
59
100
  const MOL_KEY = 'Mol'
60
101
  const MOL_VAL = '42'
@@ -62,19 +103,72 @@ test('httpStream', async t => {
62
103
  const headers = {} as { [idx: string]: string }
63
104
  headers[MOL_KEY] = MOL_VAL
64
105
 
65
- const res = await httpStream(URL, headers)
106
+ const res = await httpStream(`${host}/headers`, headers)
66
107
 
67
108
  const buffer = await streamToBuffer(res)
68
109
  const obj = JSON.parse(buffer.toString())
69
- t.equal(obj.headers[MOL_KEY], MOL_VAL, 'should send the header right')
110
+ // Node 会把 header name 规范成小写
111
+ t.equal(obj.headers[MOL_KEY.toLowerCase()], MOL_VAL, 'should send the header right')
70
112
  })
71
113
 
72
114
  test('httpStream in chunks', async (t) => {
73
- const URL = 'https://media.w3.org/2010/05/sintel/trailer.mp4'
74
- const res = await httpStream(URL)
75
- let length = 0
76
- for await (const chunk of res) {
77
- length += chunk.length
115
+ const FILE_SIZE = 1024 * 1024 + 123 // > 默认 chunk size 触发分片逻辑
116
+ const content = Buffer.alloc(FILE_SIZE, 'A')
117
+
118
+ const server = createServer((req, res) => {
119
+ // HEAD:让 httpStream 判断是否支持 range + content-length
120
+ if (req.method === 'HEAD') {
121
+ res.writeHead(200, {
122
+ 'Accept-Ranges': 'bytes',
123
+ 'Content-Length': String(FILE_SIZE),
124
+ })
125
+ res.end()
126
+ return
127
+ }
128
+
129
+ const range = req.headers.range
130
+ if (range) {
131
+ const m = String(range).match(/bytes=(\d+)-(\d+)/)
132
+ if (!m) {
133
+ res.writeHead(416)
134
+ res.end()
135
+ return
136
+ }
137
+ const start = Number(m[1])
138
+ const end = Number(m[2])
139
+ const chunk = content.subarray(start, end + 1)
140
+ res.writeHead(206, {
141
+ 'Accept-Ranges': 'bytes',
142
+ 'Content-Length': String(chunk.length),
143
+ 'Content-Range': `bytes ${start}-${end}/${FILE_SIZE}`,
144
+ })
145
+ res.end(chunk)
146
+ return
147
+ }
148
+
149
+ res.writeHead(200, {
150
+ 'Accept-Ranges': 'bytes',
151
+ 'Content-Length': String(FILE_SIZE),
152
+ })
153
+ res.end(content)
154
+ })
155
+
156
+ const host = await new Promise<string>((resolve) => {
157
+ server.listen(0, '127.0.0.1', () => {
158
+ const addr = server.address() as AddressInfo
159
+ resolve(`http://127.0.0.1:${addr.port}`)
160
+ })
161
+ })
162
+ t.teardown(() => { server.close() })
163
+
164
+ const originalChunkSize = process.env['FILEBOX_HTTP_CHUNK_SIZE']
165
+ process.env['FILEBOX_HTTP_CHUNK_SIZE'] = String(256 * 1024)
166
+ try {
167
+ const res = await httpStream(`${host}/file`)
168
+ const buffer = await streamToBuffer(res)
169
+ t.equal(buffer.length, FILE_SIZE, 'should get data in chunks right')
170
+ } finally {
171
+ if (originalChunkSize) process.env['FILEBOX_HTTP_CHUNK_SIZE'] = originalChunkSize
172
+ else delete process.env['FILEBOX_HTTP_CHUNK_SIZE']
78
173
  }
79
- t.equal(length, 4372373, 'should get data in chunks right')
80
174
  })
package/src/misc.ts CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  HTTP_CHUNK_SIZE,
16
16
  HTTP_REQUEST_TIMEOUT,
17
17
  HTTP_RESPONSE_TIMEOUT,
18
- NO_SLICE_DOWN,
19
18
  } from './config.js'
20
19
 
21
20
  const protocolMap: {
@@ -25,6 +24,7 @@ const protocolMap: {
25
24
  'https:': { agent: https.globalAgent, request: https.request },
26
25
  }
27
26
 
27
+ const noop = () => { }
28
28
  const unsupportedRangeDomains = new Set<string>()
29
29
 
30
30
  function getProtocol (protocol: string) {
@@ -53,8 +53,8 @@ export async function httpHeadHeader (url: string, headers: http.OutgoingHttpHea
53
53
  }
54
54
 
55
55
  const res = await fetch(url, {
56
- method: 'HEAD',
57
56
  headers,
57
+ method: 'HEAD',
58
58
  }, proxyUrl)
59
59
  res.destroy()
60
60
 
@@ -71,7 +71,8 @@ export async function httpHeadHeader (url: string, headers: http.OutgoingHttpHea
71
71
  throw new Error('302 found but no location!')
72
72
  }
73
73
 
74
- url = res.headers.location
74
+ // Location 可能是相对路径,需要以当前 url 作为 base 解析
75
+ url = new URL(res.headers.location, url).toString()
75
76
  }
76
77
  }
77
78
 
@@ -107,8 +108,12 @@ export async function httpStream (url: string, headers: http.OutgoingHttpHeaders
107
108
 
108
109
  const fileSize = Number(headHeaders['content-length'])
109
110
 
110
- if (!unsupportedRangeDomains.has(hostname)! && !NO_SLICE_DOWN && headHeaders['accept-ranges'] === 'bytes' && fileSize > HTTP_CHUNK_SIZE) {
111
- return await downloadFileInChunks(url, options, fileSize, HTTP_CHUNK_SIZE, proxyUrl)
111
+ // 运行时读取 env:方便测试/调用方动态调整
112
+ const noSliceDown = process.env['FILEBOX_NO_SLICE_DOWN'] === 'true'
113
+ const chunkSize = Number(process.env['FILEBOX_HTTP_CHUNK_SIZE']) || HTTP_CHUNK_SIZE
114
+
115
+ if (!unsupportedRangeDomains.has(hostname) && !noSliceDown && headHeaders['accept-ranges'] === 'bytes' && fileSize > chunkSize) {
116
+ return await downloadFileInChunks(url, options, fileSize, chunkSize, proxyUrl)
112
117
  } else {
113
118
  return await fetch(url, options, proxyUrl)
114
119
  }
@@ -117,32 +122,81 @@ export async function httpStream (url: string, headers: http.OutgoingHttpHeaders
117
122
  async function fetch (url: string, options: http.RequestOptions, proxyUrl?: string): Promise<http.IncomingMessage> {
118
123
  const { protocol } = new URL(url)
119
124
  const { request, agent } = getProtocol(protocol)
125
+ const abortController = new AbortController()
126
+ const signal = abortController.signal
120
127
  const opts: http.RequestOptions = {
121
128
  agent,
122
129
  ...options,
130
+ signal,
123
131
  }
124
132
  setProxy(opts, proxyUrl)
133
+
125
134
  const req = request(url, opts)
126
- req
127
- .on('error', () => {
128
- req.destroy()
135
+ let res: http.IncomingMessage | undefined
136
+
137
+ // 兜底:任何时候 req.error 都不会变成 uncaughtException
138
+ const onReqError = (err: unknown) => {
139
+ const error = err instanceof Error ? err : new Error(String(err))
140
+ // 统一用 abort 中止请求:signal 已挂在 req 上,并且我们会把 abort(reason) 桥接到 res.destroy(...)
141
+ abortController.abort(error)
142
+ }
143
+ req.on('error', noop)
144
+ .on('error', onReqError)
145
+ .once('close', () => {
146
+ // close 后禁用 request timeout,避免定时器晚到触发 destroy -> error 无人监听
147
+ try { req.setTimeout(0) } catch { }
148
+ req.off('error', onReqError)
149
+ req.off('error', noop)
129
150
  })
151
+ // request timeout:只用于“拿到 response 之前”(连接/握手/首包)
130
152
  .setTimeout(HTTP_REQUEST_TIMEOUT, () => {
131
- req.emit('error', new Error(`FileBox: Http request timeout (${HTTP_REQUEST_TIMEOUT})!`))
153
+ // 已经拿到 response 时,不要再用 request timeout 误伤(会导致 aborted/ECONNRESET)
154
+ if (res) return
155
+ abortController.abort(new Error(`FileBox: Http request timeout (${HTTP_REQUEST_TIMEOUT})!`))
132
156
  })
133
157
  .end()
134
- const responseEvents = await once(req, 'response')
135
- const res = responseEvents[0] as http.IncomingMessage
136
- res
137
- .on('error', () => {
138
- res.destroy()
158
+
159
+ try {
160
+ const responseEvent = await once(req, 'response', { signal })
161
+ res = responseEvent[0] as http.IncomingMessage
162
+ // response 到来后清掉 request timeout,避免误伤长下载导致 aborted/ECONNRESET
163
+ try { req.setTimeout(0) } catch {}
164
+ // 必须尽早挂,避免 “response 刚到就 error/abort” 的竞态导致 uncaughtException
165
+ res.on('error', noop)
166
+ signal.throwIfAborted()
167
+ } catch (e) {
168
+ // once(...) 被 signal abort 时通常会抛 AbortError;优先抛出 abort(reason) 的真实原因
169
+ const reason = signal.reason as unknown
170
+ const err = reason instanceof Error
171
+ ? reason
172
+ : (e instanceof Error ? e : new Error(String(e)))
173
+ // 失败时尽量主动清理,避免 socket 悬挂(destroy 重复调用是安全的)
174
+ try { res?.destroy(err) } catch {}
175
+ try { req.destroy(err) } catch {}
176
+ throw err
177
+ }
178
+
179
+ const onAbort = () => {
180
+ const reason = signal.reason as unknown
181
+ res?.destroy(reason instanceof Error ? reason : new Error(String(reason)))
182
+ }
183
+ signal.addEventListener('abort', onAbort, { once: true })
184
+ res!
185
+ .once('end', () => { try { res!.setTimeout(0) } catch { } })
186
+ .once('close', () => {
187
+ // close 时做清理/兜底判断(尽力而为)
188
+ try { res!.setTimeout(0) } catch { }
189
+ if (!res!.complete && !res!.destroyed) {
190
+ // 有些场景不会 emit 'aborted',用 close + complete 兜底一次
191
+ res!.destroy(new Error('FileBox: Http response aborted!'))
192
+ }
193
+ signal.removeEventListener('abort', onAbort)
194
+ res!.off('error', noop)
139
195
  })
140
- if (res.socket) {
141
- res.setTimeout(HTTP_RESPONSE_TIMEOUT, () => {
142
- res.emit('error', new Error(`FileBox: Http response timeout (${HTTP_RESPONSE_TIMEOUT})!`))
196
+ .setTimeout(HTTP_RESPONSE_TIMEOUT, () => {
197
+ abortController.abort(new Error(`FileBox: Http response timeout (${HTTP_RESPONSE_TIMEOUT})!`))
143
198
  })
144
- }
145
- return res
199
+ return res!
146
200
  }
147
201
 
148
202
  async function downloadFileInChunks (
@@ -154,6 +208,12 @@ async function downloadFileInChunks (
154
208
  ): Promise<Readable> {
155
209
  const tmpFile = join(tmpdir(), `filebox-${randomUUID()}`)
156
210
  const writeStream = createWriteStream(tmpFile)
211
+ const writeAbortController = new AbortController()
212
+ const signal = writeAbortController.signal
213
+ const onWriteError = (err: unknown) => {
214
+ writeAbortController.abort(err instanceof Error ? err : new Error(String(err)))
215
+ }
216
+ writeStream.once('error', onWriteError)
157
217
  const allowStatusCode = [ 200, 206 ]
158
218
  const requestBaseOptions: http.RequestOptions = {
159
219
  headers: {},
@@ -177,7 +237,10 @@ async function downloadFileInChunks (
177
237
  if (res.statusCode === 416) {
178
238
  unsupportedRangeDomains.add(new URL(url).hostname)
179
239
  // 某些云服务商对分片下载的支持可能不规范,需要保留一个回退的方式
180
- writeStream.close()
240
+ writeStream.destroy()
241
+ try {
242
+ await once(writeStream, 'close', { signal })
243
+ } catch {}
181
244
  await rm(tmpFile, { force: true })
182
245
  return await fetch(url, requestBaseOptions, proxyUrl)
183
246
  }
@@ -194,13 +257,20 @@ async function downloadFileInChunks (
194
257
  for await (const chunk of res) {
195
258
  assert(Buffer.isBuffer(chunk))
196
259
  downSize += chunk.length
197
- writeStream.write(chunk)
260
+ if (!writeStream.write(chunk)) {
261
+ try {
262
+ await once(writeStream, 'drain', { signal })
263
+ } catch (e) {
264
+ const reason = signal.reason as unknown
265
+ throw reason instanceof Error ? reason : (e as Error)
266
+ }
267
+ }
198
268
  }
199
269
  res.destroy()
200
270
  } catch (error) {
201
- const err = error as Error
271
+ const err = error instanceof Error ? error : new Error(String(error))
202
272
  if (--retries <= 0) {
203
- writeStream.close()
273
+ writeStream.destroy()
204
274
  void rm(tmpFile, { force: true })
205
275
  throw new Error(`Download file with chunk failed! ${err.message}`, { cause: err })
206
276
  }
@@ -208,14 +278,24 @@ async function downloadFileInChunks (
208
278
  chunkSeq++
209
279
  start = downSize
210
280
  }
211
- writeStream.close()
281
+
282
+ writeStream.end()
283
+ try {
284
+ await once(writeStream, 'finish', { signal })
285
+ } catch (e) {
286
+ const reason = signal.reason as unknown
287
+ if (reason instanceof Error) {
288
+ throw reason
289
+ }
290
+ throw e
291
+ } finally {
292
+ writeStream.off('error', onWriteError)
293
+ }
212
294
 
213
295
  const readStream = createReadStream(tmpFile)
214
- readStream
215
- .once('end', () => readStream.close())
216
- .once('close', () => {
217
- void rm(tmpFile, { force: true })
218
- })
296
+ readStream.once('close', () => {
297
+ rm(tmpFile, { force: true }).catch(() => {})
298
+ })
219
299
  return readStream
220
300
  }
221
301
 
@@ -234,15 +314,14 @@ function setProxy (options: RequestOptions, proxyUrl?: string): void {
234
314
  }
235
315
  }
236
316
 
237
-
238
317
  function parseContentRange (contentRange: string): { start: number, end: number, total: number } {
239
318
  const matches = contentRange.match(/bytes (\d+)-(\d+)\/(\d+)/)
240
319
  if (!matches) {
241
320
  throw new Error('Invalid content range')
242
321
  }
243
322
  return {
244
- start: Number(matches[1]),
245
323
  end: Number(matches[2]),
324
+ start: Number(matches[1]),
246
325
  total: Number(matches[3]),
247
326
  }
248
327
  }
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * This file was auto generated from scripts/generate-version.sh
3
3
  */
4
- export const VERSION: string = '1.7.19'
4
+ export const VERSION: string = '1.7.20'