This repository has been archived by the owner on Jan 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
bootstrap.js
138 lines (127 loc) · 3.62 KB
/
bootstrap.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { readFile } from './fs/promises.js'
import { createWriteStream } from './fs/index.js'
import { PassThrough } from './stream.js'
import { createDigest } from './crypto.js'
import { EventEmitter } from './events.js'
async function * streamAsyncIterable (stream) {
const reader = stream.getReader()
try {
while (true) {
const { done, value } = await reader.read()
if (done) return
yield value
}
} finally {
reader.releaseLock()
}
}
/**
* @param {Buffer|String} buf
* @param {string} hashAlgorithm
* @returns {Promise<string>}
*/
async function getHash (buf, hashAlgorithm) {
const digest = await createDigest(hashAlgorithm, buf)
return digest.toString('hex')
}
/**
* @param {string} dest - file path
* @param {string} hash - hash string
* @param {string} hashAlgorithm - hash algorithm
* @returns {Promise<boolean>}
*/
export async function checkHash (dest, hash, hashAlgorithm) {
let buf
try {
buf = await readFile(dest)
} catch (err) {
// download if file is corrupted or does not exist
return false
}
return hash === await getHash(buf, hashAlgorithm)
}
class Bootstrap extends EventEmitter {
constructor (options) {
super()
if (!options.url || !options.dest) {
throw new Error('.url and .dest are required string properties on the object provided to the constructor at the first argument position')
}
this.options = options
}
async run () {
try {
const fileBuffer = await this.download(this.options.url)
await this.write({ fileBuffer, dest: this.options.dest })
} catch (err) {
this.emit('error', err)
throw err
} finally {
this.cleanup()
}
}
/**
* @param {object} options
* @param {Uint8Array} options.fileBuffer
* @param {string} options.dest
* @returns {Promise<void>}
*/
async write ({ fileBuffer, dest }) {
this.emit('write-file', { status: 'started' })
const passThroughStream = new PassThrough()
const writeStream = createWriteStream(dest, { mode: 0o755 })
passThroughStream.pipe(writeStream)
let written = 0
passThroughStream.on('data', data => {
written += data.length
const progress = written / fileBuffer.byteLength
this.emit('write-file-progress', progress)
})
passThroughStream.write(fileBuffer)
passThroughStream.end()
return new Promise((resolve, reject) => {
writeStream.on('finish', () => {
this.emit('write-file', { status: 'finished' })
resolve()
})
writeStream.on('error', err => {
this.emit('error', err)
reject(err)
})
})
}
/**
* @param {string} url - url to download
* @returns {Promise<Uint8Array>}
* @throws {Error} - if status code is not 200
*/
async download (url) {
const response = await fetch(url, { mode: 'cors' })
if (!response.ok) {
throw new Error(`Bootstrap request failed: ${response.status} ${response.statusText}`)
}
const contentLength = +response.headers.get('Content-Length')
let receivedLength = 0
let prevProgress = 0
const fileData = new Uint8Array(contentLength)
for await (const chunk of streamAsyncIterable(response.body)) {
fileData.set(chunk, receivedLength)
receivedLength += chunk.length
const progress = (receivedLength / contentLength * 100) | 0
if (progress !== prevProgress) {
this.emit('download-progress', progress)
prevProgress = progress
}
}
return fileData
}
cleanup () {
this.removeAllListeners()
}
}
export function bootstrap (options) {
return new Bootstrap(options)
}
export default {
bootstrap,
checkHash
}