Skip to content

Commit a461e65

Browse files
authored
Report file download progress with readFile (#3)
1 parent 7421a81 commit a461e65

File tree

3 files changed

+77
-10
lines changed

3 files changed

+77
-10
lines changed

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default tseslint.config(
5656
'unicorn/prefer-modern-math-apis': 'off',
5757
'unicorn/prefer-node-protocol': 'off',
5858
'unicorn/no-unreadable-array-destructuring': 'off',
59+
'unicorn/text-encoding-identifier-case': 'off',
5960
'unicorn/no-abusive-eslint-disable': 'off',
6061
'unicorn/no-array-callback-reference': 'off',
6162
'unicorn/number-literal-case': 'off',

src/filehandle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface FilehandleOptions {
1111
headers?: any
1212
overrides?: any
1313
encoding?: BufferEncoding
14+
statusCallback?: (arg: string) => void
1415
/**
1516
* fetch function to use for HTTP requests. defaults to environment's
1617
* global fetch. if there is no global fetch, and a fetch function is not provided,

src/remoteFile.ts

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,39 @@ function getMessage(e: unknown) {
1313
return r.replace(/\.$/, '')
1414
}
1515

16+
export function toLocale(n: number) {
17+
return n.toLocaleString('en-US')
18+
}
19+
20+
function r(s: number) {
21+
return toLocale(Number.parseFloat(s.toPrecision(3)))
22+
}
23+
function sum(array: Uint8Array[]) {
24+
let sum = 0
25+
for (const entry of array) {
26+
sum += entry.length
27+
}
28+
return sum
29+
}
30+
function concatUint8Array(args: Uint8Array[]) {
31+
const mergedArray = new Uint8Array(sum(args))
32+
let offset = 0
33+
for (const entry of args) {
34+
mergedArray.set(entry, offset)
35+
offset += entry.length
36+
}
37+
return mergedArray
38+
}
39+
export function getProgressDisplayStr(current: number, total: number) {
40+
if (Math.floor(total / 1_000_000) > 0) {
41+
return `${r(current / 1_000_000)}/${r(total / 1_000_000)}Mb`
42+
} else if (Math.floor(total / 1_000) > 0) {
43+
return `${r(current / 1_000)}/${r(total / 1_000)}Kb`
44+
} else {
45+
return `${r(current)}/${r(total)}}bytes`
46+
}
47+
}
48+
1649
export default class RemoteFile implements GenericFilehandle {
1750
protected url: string
1851
private _stat?: Stats
@@ -89,8 +122,6 @@ export default class RemoteFile implements GenericFilehandle {
89122
}
90123

91124
if ((res.status === 200 && position === 0) || res.status === 206) {
92-
const resData = await res.arrayBuffer()
93-
94125
// try to parse out the size of the remote file
95126
const contentRange = res.headers.get('content-range')
96127
const sizeMatch = /\/(\d+)$/.exec(contentRange || '')
@@ -99,8 +130,7 @@ export default class RemoteFile implements GenericFilehandle {
99130
size: parseInt(sizeMatch[1], 10),
100131
}
101132
}
102-
103-
return new Uint8Array(resData.slice(0, length))
133+
return new Uint8Array(await res.arrayBuffer())
104134
}
105135

106136
// eslint-disable-next-line unicorn/prefer-ternary
@@ -139,7 +169,7 @@ export default class RemoteFile implements GenericFilehandle {
139169
opts = options
140170
delete opts.encoding
141171
}
142-
const { headers = {}, signal, overrides = {} } = opts
172+
const { statusCallback, headers = {}, signal, overrides = {} } = opts
143173
const res = await this.fetch(this.url, {
144174
headers,
145175
method: 'GET',
@@ -152,12 +182,47 @@ export default class RemoteFile implements GenericFilehandle {
152182
if (res.status !== 200) {
153183
throw new Error(`HTTP ${res.status} fetching ${this.url}`)
154184
}
155-
if (encoding === 'utf8') {
156-
return res.text()
157-
} else if (encoding) {
158-
throw new Error(`unsupported encoding: ${encoding}`)
185+
// Get the total size for progress reporting
186+
const contentLength = res.headers.get('content-length')
187+
const totalBytes = contentLength ? parseInt(contentLength, 10) : undefined
188+
189+
if (statusCallback && res.body && totalBytes) {
190+
const reader = res.body.getReader()
191+
const chunks: Uint8Array[] = []
192+
let receivedBytes = 0
193+
194+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
195+
while (true) {
196+
const { done, value } = await reader.read()
197+
198+
if (done) {
199+
break
200+
}
201+
202+
chunks.push(value)
203+
receivedBytes += value.length
204+
205+
statusCallback(
206+
`Downloading ${getProgressDisplayStr(receivedBytes, totalBytes)}`,
207+
)
208+
}
209+
210+
if (encoding === 'utf8' || encoding === 'utf-8') {
211+
const decoder = new TextDecoder('utf-8')
212+
return decoder.decode(concatUint8Array(chunks))
213+
} else if (encoding) {
214+
throw new Error(`unsupported encoding: ${encoding}`)
215+
} else {
216+
return concatUint8Array(chunks)
217+
}
159218
} else {
160-
return new Uint8Array(await res.arrayBuffer())
219+
if (encoding === 'utf8' || encoding === 'utf-8') {
220+
return res.text()
221+
} else if (encoding) {
222+
throw new Error(`unsupported encoding: ${encoding}`)
223+
} else {
224+
return new Uint8Array(await res.arrayBuffer())
225+
}
161226
}
162227
}
163228

0 commit comments

Comments
 (0)