Add initial minecraft-java-manager scaffold
Initial project scaffold for minecraft-java-manager. Adds package.json and package-lock.json, README update, index.js entrypoint and test.js. Introduces src modules: checker (detects Java version), installer (downloads JRE components with redirects, SHA1 validation, progress/events and concurrency), web (fetches launcher/runtime manifests and component data), systeminfo (platform key and install path resolution) and config (endpoints). Declares dependencies adm-zip and node-downloader-helper.
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
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 emitter
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
downloadJavaComponent
|
||||
}
|
||||
Reference in New Issue
Block a user