@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.
- package/dist/cjs/src/config.d.ts +1 -0
- package/dist/cjs/src/config.d.ts.map +1 -1
- package/dist/cjs/src/config.js +3 -1
- package/dist/cjs/src/config.js.map +1 -1
- package/dist/cjs/src/file-box.d.ts.map +1 -1
- package/dist/cjs/src/file-box.js +4 -1
- package/dist/cjs/src/file-box.js.map +1 -1
- package/dist/cjs/src/file-box.spec.js +3 -4
- package/dist/cjs/src/file-box.spec.js.map +1 -1
- package/dist/cjs/src/file-box.type.js +1 -2
- package/dist/cjs/src/file-box.type.js.map +1 -1
- package/dist/cjs/src/misc.d.ts +3 -3
- package/dist/cjs/src/misc.d.ts.map +1 -1
- package/dist/cjs/src/misc.js +132 -4
- package/dist/cjs/src/misc.js.map +1 -1
- package/dist/cjs/src/misc.spec.js +9 -0
- package/dist/cjs/src/misc.spec.js.map +1 -1
- package/dist/cjs/src/version.js +1 -1
- package/dist/cjs/tests/network-timeout.spec.js +6 -2
- package/dist/cjs/tests/network-timeout.spec.js.map +1 -1
- package/dist/esm/src/config.d.ts +1 -0
- package/dist/esm/src/config.d.ts.map +1 -1
- package/dist/esm/src/config.js +2 -0
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/file-box.d.ts.map +1 -1
- package/dist/esm/src/file-box.js +4 -1
- package/dist/esm/src/file-box.js.map +1 -1
- package/dist/esm/src/file-box.spec.js +1 -2
- package/dist/esm/src/file-box.spec.js.map +1 -1
- package/dist/esm/src/misc.d.ts +3 -3
- package/dist/esm/src/misc.d.ts.map +1 -1
- package/dist/esm/src/misc.js +132 -4
- package/dist/esm/src/misc.js.map +1 -1
- package/dist/esm/src/misc.spec.js +9 -0
- package/dist/esm/src/misc.spec.js.map +1 -1
- package/dist/esm/src/version.js +1 -1
- package/dist/esm/tests/network-timeout.spec.js +6 -2
- package/dist/esm/tests/network-timeout.spec.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +3 -0
- package/src/file-box.ts +4 -1
- package/src/misc.spec.ts +10 -0
- package/src/misc.ts +166 -10
- 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 {
|
|
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<
|
|
100
|
+
): Promise<Readable> {
|
|
100
101
|
const parsedUrl = new URL(url)
|
|
101
102
|
|
|
102
|
-
const 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
|
-
|
|
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(
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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