247 lines
7.1 KiB
JavaScript
247 lines
7.1 KiB
JavaScript
const https = require("https")
|
|
const fs = require("fs")
|
|
const path = require("path")
|
|
const crypto = require("crypto")
|
|
const { EventEmitter } = require("events")
|
|
const web = require("./web")
|
|
const sysinfo = require("./systeminfo")
|
|
|
|
function extractDownloadableFiles(manifest) {
|
|
const filePaths = Object.keys(manifest.files)
|
|
const entries = []
|
|
|
|
for (const filePath of filePaths) {
|
|
const fileEntry = manifest.files[filePath]
|
|
|
|
if (fileEntry.type !== "file") {
|
|
continue
|
|
}
|
|
|
|
const downloadInfo = fileEntry.downloads.raw || fileEntry.downloads.lzma
|
|
|
|
if (!downloadInfo) {
|
|
continue
|
|
}
|
|
|
|
entries.push({
|
|
filePath: filePath,
|
|
url: downloadInfo.url,
|
|
sha1: downloadInfo.sha1,
|
|
size: downloadInfo.size,
|
|
executable: fileEntry.executable || false
|
|
})
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
function requestWithRedirect(url, onResponse, maxRedirects = 5) {
|
|
return new Promise((resolve, reject) => {
|
|
|
|
function makeRequest(currentUrl, redirectsLeft) {
|
|
https.get(currentUrl, (response) => {
|
|
|
|
const statusCode = response.statusCode
|
|
|
|
if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
|
|
response.resume()
|
|
|
|
if (redirectsLeft <= 0) {
|
|
reject(new Error("Too many http redirection"))
|
|
return
|
|
}
|
|
|
|
makeRequest(response.headers.location, redirectsLeft - 1)
|
|
return
|
|
}
|
|
|
|
if (statusCode !== 200) {
|
|
response.resume()
|
|
reject(new Error(`HTTP Failed: received status code ${statusCode} for ${currentUrl}`))
|
|
return
|
|
}
|
|
|
|
try {
|
|
onResponse(response)
|
|
resolve()
|
|
} catch (err) {
|
|
reject(err)
|
|
}
|
|
|
|
}).on("error", (err) => {
|
|
reject(err)
|
|
})
|
|
}
|
|
|
|
makeRequest(url, maxRedirects)
|
|
})
|
|
}
|
|
async function downloadSingleFile(fileEntry, outputRoot, onProgress) {
|
|
const outputPath = path.join(outputRoot, fileEntry.filePath)
|
|
|
|
await fs.promises.mkdir(path.dirname(outputPath), {
|
|
recursive: true
|
|
})
|
|
|
|
const hasher = crypto.createHash("sha1")
|
|
const writeStream = fs.createWriteStream(outputPath)
|
|
|
|
let downloadedBytes = 0
|
|
|
|
try {
|
|
await requestWithRedirect(fileEntry.url, (responseStream) => {
|
|
responseStream.on("data", (chunk) => {
|
|
downloadedBytes += chunk.length
|
|
hasher.update(chunk)
|
|
|
|
if (onProgress) {
|
|
onProgress({
|
|
filePath: fileEntry.filePath,
|
|
bytesDownloaded: downloadedBytes,
|
|
totalBytes: fileEntry.size
|
|
})
|
|
}
|
|
})
|
|
|
|
responseStream.pipe(writeStream)
|
|
})
|
|
|
|
await new Promise((resolve, reject) => {
|
|
writeStream.on("finish", resolve)
|
|
writeStream.on("error", reject)
|
|
})
|
|
|
|
const computedSha1 = hasher.digest("hex")
|
|
|
|
if (computedSha1 !== fileEntry.sha1) {
|
|
await fs.promises.unlink(outputPath).catch(() => {})
|
|
throw new Error(
|
|
`Hash SHA1 invalide pour ${fileEntry.filePath}. ` +
|
|
`Attendu: ${fileEntry.sha1}, Obtenu: ${computedSha1}`
|
|
)
|
|
}
|
|
|
|
if (fileEntry.executable && process.platform !== "win32") {
|
|
await fs.promises.chmod(outputPath, 0o755).catch(() => {})
|
|
}
|
|
|
|
return {
|
|
filePath: fileEntry.filePath,
|
|
outputPath: outputPath,
|
|
sha1: computedSha1,
|
|
totalBytes: downloadedBytes
|
|
}
|
|
|
|
} catch (err) {
|
|
if (!writeStream.destroyed) {
|
|
writeStream.destroy()
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function downloadJavaComponent(manifest, outputRoot, options) {
|
|
const emitter = new EventEmitter()
|
|
const concurrency = (options && options.concurrency) || 5
|
|
|
|
try {
|
|
const files = extractDownloadableFiles(manifest)
|
|
const totalFiles = files.length
|
|
const totalBytesExpected = files.reduce((sum, f) => { return sum + (f.size || 0) }, 0)
|
|
|
|
emitter.emit("start", {
|
|
totalFiles: totalFiles,
|
|
totalBytes: totalBytesExpected,
|
|
outputRoot: outputRoot
|
|
})
|
|
|
|
let completedFiles = 0
|
|
let failedFiles = 0
|
|
let totalBytesDownloaded = 0
|
|
const errors = []
|
|
|
|
const perFileProgress = {}
|
|
|
|
function reportGlobalProgress() {
|
|
const currentTotal = Object.keys(perFileProgress).reduce((sum, key) => {
|
|
return sum + perFileProgress[key]
|
|
}, 0)
|
|
|
|
emitter.emit("progress", {
|
|
completedFiles: completedFiles,
|
|
totalFiles: totalFiles,
|
|
bytesDownloaded: currentTotal,
|
|
totalBytes: totalBytesExpected,
|
|
percent: totalBytesExpected > 0 ?
|
|
Math.round((currentTotal / totalBytesExpected) * 100) :
|
|
null
|
|
})
|
|
}
|
|
|
|
let cursor = 0
|
|
|
|
async function worker() {
|
|
while (cursor < files.length) {
|
|
const currentIndex = cursor
|
|
cursor += 1
|
|
const fileEntry = files[currentIndex]
|
|
|
|
try {
|
|
const result = await downloadSingleFile(fileEntry, outputRoot, (progressInfo) => {
|
|
perFileProgress[progressInfo.filePath] = progressInfo.bytesDownloaded
|
|
reportGlobalProgress()
|
|
})
|
|
|
|
completedFiles += 1
|
|
totalBytesDownloaded += result.totalBytes
|
|
perFileProgress[fileEntry.filePath] = result.totalBytes
|
|
|
|
emitter.emit("fileComplete", result)
|
|
reportGlobalProgress()
|
|
|
|
} catch (err) {
|
|
failedFiles += 1
|
|
errors.push({
|
|
filePath: fileEntry.filePath,
|
|
message: err.message
|
|
})
|
|
|
|
emitter.emit("fileError", {
|
|
filePath: fileEntry.filePath,
|
|
message: err.message,
|
|
error: err
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const workerCount = Math.min(concurrency, files.length)
|
|
const workers = []
|
|
|
|
for (let i = 0; i < workerCount; i += 1) {
|
|
workers.push(worker())
|
|
}
|
|
|
|
await Promise.all(workers)
|
|
|
|
emitter.emit("complete", {
|
|
totalFiles: totalFiles,
|
|
completedFiles: completedFiles,
|
|
failedFiles: failedFiles,
|
|
totalBytes: totalBytesDownloaded,
|
|
errors: errors
|
|
})
|
|
|
|
} catch (err) {
|
|
emitter.emit("error", {
|
|
message: err.message,
|
|
error: err
|
|
})
|
|
}
|
|
|
|
return { events: emitter, path: outputRoot }
|
|
}
|
|
|
|
module.exports = {
|
|
downloadJavaComponent
|
|
} |