@juzi/file-box 1.7.1 → 1.7.3

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 (44) hide show
  1. package/dist/cjs/src/config.d.ts +1 -0
  2. package/dist/cjs/src/config.d.ts.map +1 -1
  3. package/dist/cjs/src/config.js +3 -1
  4. package/dist/cjs/src/config.js.map +1 -1
  5. package/dist/cjs/src/file-box.d.ts.map +1 -1
  6. package/dist/cjs/src/file-box.js +4 -1
  7. package/dist/cjs/src/file-box.js.map +1 -1
  8. package/dist/cjs/src/file-box.spec.js +3 -4
  9. package/dist/cjs/src/file-box.spec.js.map +1 -1
  10. package/dist/cjs/src/file-box.type.js +1 -2
  11. package/dist/cjs/src/file-box.type.js.map +1 -1
  12. package/dist/cjs/src/misc.d.ts +3 -3
  13. package/dist/cjs/src/misc.d.ts.map +1 -1
  14. package/dist/cjs/src/misc.js +132 -4
  15. package/dist/cjs/src/misc.js.map +1 -1
  16. package/dist/cjs/src/misc.spec.js +9 -0
  17. package/dist/cjs/src/misc.spec.js.map +1 -1
  18. package/dist/cjs/src/version.js +1 -1
  19. package/dist/cjs/tests/network-timeout.spec.js +6 -2
  20. package/dist/cjs/tests/network-timeout.spec.js.map +1 -1
  21. package/dist/esm/src/config.d.ts +1 -0
  22. package/dist/esm/src/config.d.ts.map +1 -1
  23. package/dist/esm/src/config.js +2 -0
  24. package/dist/esm/src/config.js.map +1 -1
  25. package/dist/esm/src/file-box.d.ts.map +1 -1
  26. package/dist/esm/src/file-box.js +4 -1
  27. package/dist/esm/src/file-box.js.map +1 -1
  28. package/dist/esm/src/file-box.spec.js +1 -2
  29. package/dist/esm/src/file-box.spec.js.map +1 -1
  30. package/dist/esm/src/misc.d.ts +3 -3
  31. package/dist/esm/src/misc.d.ts.map +1 -1
  32. package/dist/esm/src/misc.js +132 -4
  33. package/dist/esm/src/misc.js.map +1 -1
  34. package/dist/esm/src/misc.spec.js +9 -0
  35. package/dist/esm/src/misc.spec.js.map +1 -1
  36. package/dist/esm/src/version.js +1 -1
  37. package/dist/esm/tests/network-timeout.spec.js +6 -2
  38. package/dist/esm/tests/network-timeout.spec.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/config.ts +3 -0
  41. package/src/file-box.ts +4 -1
  42. package/src/misc.spec.ts +10 -0
  43. package/src/misc.ts +166 -10
  44. package/src/version.ts +1 -1
package/src/misc.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import http from 'http'
2
2
  import https from 'https'
3
3
  import { URL } from 'url'
4
- import type stream from 'stream'
5
4
 
6
- import { HTTP_TIMEOUT } from './config.js'
5
+ import { PassThrough, pipeline, Readable } from 'stream'
6
+
7
+ import { HTTP_CHUNK_SIZE, HTTP_TIMEOUT } from './config.js'
7
8
 
8
9
  export function dataUrlToBase64 (dataUrl: string): string {
9
10
  const dataList = dataUrl.split(',')
@@ -96,10 +97,10 @@ export function httpHeaderToFileName (
96
97
  export async function httpStream (
97
98
  url : string,
98
99
  headers : http.OutgoingHttpHeaders = {},
99
- ): Promise<http.IncomingMessage> {
100
+ ): Promise<Readable> {
100
101
  const parsedUrl = new URL(url)
101
102
 
102
- const protocol = parsedUrl.protocol
103
+ const protocol = parsedUrl.protocol
103
104
 
104
105
  const options: http.RequestOptions = {}
105
106
 
@@ -123,12 +124,29 @@ export async function httpStream (
123
124
  ...headers,
124
125
  }
125
126
 
126
- return new Promise<http.IncomingMessage>((resolve, reject) => {
127
+ const headHeaders = await httpHeadHeader(url)
128
+ const fileSize = Number(headHeaders['content-length'])
129
+
130
+ if (headHeaders['accept-ranges'] === 'bytes' && fileSize > HTTP_CHUNK_SIZE) {
131
+ return await downloadFileInChunks(get, url, options, fileSize, HTTP_CHUNK_SIZE)
132
+ } else {
133
+ return await downloadFile(get, url, options)
134
+ }
135
+ }
136
+
137
+ async function downloadFile (
138
+ get: typeof https.get,
139
+ url: string,
140
+ options: http.RequestOptions,
141
+ ): Promise<Readable> {
142
+ return new Promise<Readable>((resolve, reject) => {
127
143
  let res: http.IncomingMessage | null = null
128
- const req = get(parsedUrl, options, (response) => {
144
+ const req = get(url, options, (response) => {
129
145
  res = response
130
146
  resolve(res)
131
147
  })
148
+
149
+ req
132
150
  .on('error', reject)
133
151
  .setTimeout(HTTP_TIMEOUT, () => {
134
152
  const e = new Error(`FileBox: Http request timeout (${HTTP_TIMEOUT})!`)
@@ -142,9 +160,147 @@ export async function httpStream (
142
160
  })
143
161
  }
144
162
 
145
- export async function streamToBuffer (
146
- stream: stream.Readable,
147
- ): Promise<Buffer> {
163
+ async function downloadFileInChunks (
164
+ get: typeof https.get,
165
+ url: string,
166
+ options: http.RequestOptions,
167
+ fileSize: number,
168
+ chunkSize = HTTP_CHUNK_SIZE,
169
+ ): Promise<Readable> {
170
+ const ac = new AbortController()
171
+ const stream = new PassThrough()
172
+
173
+ const abortAc = () => {
174
+ if (!ac.signal.aborted) {
175
+ ac.abort()
176
+ }
177
+ }
178
+
179
+ stream
180
+ .once('close', abortAc)
181
+ .once('error', abortAc)
182
+
183
+ const chunksCount = Math.ceil(fileSize / chunkSize)
184
+ let chunksDownloaded = 0
185
+ let dataTotalSize = 0
186
+
187
+ const doDownloadChunk = async function (i: number, retries: number) {
188
+ const start = i * chunkSize
189
+ const end = Math.min((i + 1) * chunkSize - 1, fileSize - 1)
190
+ const range = `bytes=${start}-${end}`
191
+
192
+ // console.info('doDownloadChunk() range:', range)
193
+
194
+ if (ac.signal.aborted) {
195
+ stream.destroy(new Error('Signal aborted.'))
196
+ return
197
+ }
198
+
199
+ const requestOptions: http.RequestOptions = {
200
+ ...options,
201
+ signal: ac.signal,
202
+ timeout: HTTP_TIMEOUT,
203
+ }
204
+ if (!requestOptions.headers) {
205
+ requestOptions.headers = {}
206
+ }
207
+ requestOptions.headers['Range'] = range
208
+
209
+ try {
210
+ const chunk = await downloadChunk(get, url, requestOptions, retries)
211
+ if (chunk.errored) {
212
+ throw new Error('chunk stream error')
213
+ }
214
+ if (chunk.closed) {
215
+ throw new Error('chunk stream closed')
216
+ }
217
+ if (chunk.destroyed) {
218
+ throw new Error('chunk stream destroyed')
219
+ }
220
+ const buf = await streamToBuffer(chunk)
221
+ stream.push(buf)
222
+ chunksDownloaded++
223
+ dataTotalSize += buf.length
224
+
225
+ if (chunksDownloaded === chunksCount || dataTotalSize >= fileSize) {
226
+ stream.push(null)
227
+ }
228
+ } catch (err) {
229
+ if (retries === 0) {
230
+ stream.emit('error', err)
231
+ } else {
232
+ await doDownloadChunk(i, retries - 1)
233
+ }
234
+ }
235
+ }
236
+
237
+ const doDownloadAllChunks = async function () {
238
+ for (let i = 0; i < chunksCount; i++) {
239
+ if (ac.signal.aborted) {
240
+ return
241
+ }
242
+ await doDownloadChunk(i, 3)
243
+ }
244
+ }
245
+
246
+ void doDownloadAllChunks().catch((e) => {
247
+ stream.emit('error', e)
248
+ })
249
+
250
+ return stream
251
+ }
252
+
253
+ async function downloadChunk (
254
+ get: typeof https.get,
255
+ url: string,
256
+ requestOptions: http.RequestOptions,
257
+ retries: number,
258
+ ): Promise<Readable> {
259
+ return new Promise<Readable>((resolve, reject) => {
260
+ const doRequest = (attempt: number) => {
261
+ let resolved = false
262
+ const req = get(url, requestOptions, (res) => {
263
+ const statusCode = res.statusCode ?? 0
264
+ // console.info('downloadChunk(%d) statusCode: %d rsp.headers: %o', attempt, statusCode, res.headers)
265
+
266
+ if (statusCode < 200 || statusCode >= 300) {
267
+ if (attempt < retries) {
268
+ void doRequest(attempt + 1)
269
+ } else {
270
+ reject(new Error(`Request failed with status code ${res.statusCode}`))
271
+ }
272
+ return
273
+ }
274
+
275
+ const stream = pipeline(res, new PassThrough(), () => {})
276
+ resolve(stream)
277
+ resolved = true
278
+ })
279
+
280
+ req
281
+ .once('error', (err) => {
282
+ if (resolved) {
283
+ return
284
+ }
285
+ // console.error('downloadChunk(%d) req error:', attempt, err)
286
+ if (attempt < retries) {
287
+ void doRequest(attempt + 1)
288
+ } else {
289
+ reject(err)
290
+ }
291
+ })
292
+ .setTimeout(HTTP_TIMEOUT, () => {
293
+ const e = new Error(`FileBox: Http request timeout (${HTTP_TIMEOUT})!`)
294
+ req.emit('error', e)
295
+ req.destroy()
296
+ })
297
+ .end()
298
+ }
299
+ void doRequest(0)
300
+ })
301
+ }
302
+
303
+ export async function streamToBuffer (stream: Readable): Promise<Buffer> {
148
304
  return new Promise<Buffer>((resolve, reject) => {
149
305
  const bufferList: Buffer[] = []
150
306
  stream.once('error', reject)
@@ -152,6 +308,6 @@ export async function streamToBuffer (
152
308
  const fullBuffer = Buffer.concat(bufferList)
153
309
  resolve(fullBuffer)
154
310
  })
155
- stream.on('data', buffer => bufferList.push(buffer))
311
+ stream.on('data', (buffer) => bufferList.push(buffer))
156
312
  })
157
313
  }
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.1'
4
+ export const VERSION: string = '1.7.3'