Files
minecraft-java-manager/src/installer.js
T
2026-09-06 04:35:49 +02:00

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
}