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:
@@ -1,3 +1,5 @@
|
|||||||
# minecraft-java-manager
|
# minecraft-java-manager
|
||||||
|
|
||||||
Node module to check if java is installed and install it
|
Node module to check if java is installed and install it
|
||||||
|
|
||||||
|
There is two function, read index.js
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const installer = require("./src/installer")
|
||||||
|
const checker = require("./src/checker")
|
||||||
|
const sysinfo = require("./src/systeminfo")
|
||||||
|
const web = require("./src/web")
|
||||||
|
|
||||||
|
async function isJavaInstalledAndCompatibleFor(mcVersion, customExec = "java") {
|
||||||
|
const mcVersionManifestUrl = await web.getVersionManifest(mcVersion)
|
||||||
|
const requiredJRE = await web.getRequiredJREFor(mcVersionManifestUrl)
|
||||||
|
const checkJRECommand = checker.getJavaVersionOutput(customExec)
|
||||||
|
const isJavaCompatible = checker.isJavaCompatible(checkJRECommand, requiredJRE.majorVersion)
|
||||||
|
return isJavaCompatible
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installJava(component, majorVersion, customInstallPath = null) {
|
||||||
|
const installationPath = await sysinfo.determineInstallationPath(component, majorVersion, customInstallPath)
|
||||||
|
const requiredComponentDetails = await web.getJREComponentDetails(component, sysinfo.getPlatformKey())
|
||||||
|
const requiredComponentManifest = await web.getJREComponentManifest(requiredComponentDetails[0].manifest.url)
|
||||||
|
return await installer.downloadJavaComponent(requiredComponentManifest, installationPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isJavaInstalledAndCompatibleFor,
|
||||||
|
installJava
|
||||||
|
}
|
||||||
Generated
+38
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "minecraft-java-manager",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "minecraft-java-manager",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"adm-zip": "^0.6.0",
|
||||||
|
"node-downloader-helper": "^2.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/adm-zip": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-downloader-helper": {
|
||||||
|
"version": "2.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.11.tgz",
|
||||||
|
"integrity": "sha512-882fH2C9AWdiPCwz/2beq5t8FGMZK9Dx8TJUOIxzMCbvG7XUKM5BuJwN5f0NKo4SCQK6jR4p2TPm54mYGdGchQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"ndh": "bin/ndh"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "minecraft-java-manager",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Node module to check if java is installed and install it",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.azures.fr/azures04/minecraft-java-manager"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "azures04",
|
||||||
|
"url": "https://gitea.azures.fr",
|
||||||
|
"email": "azures04@azures.fr"
|
||||||
|
},
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"adm-zip": "^0.6.0",
|
||||||
|
"node-downloader-helper": "^2.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const childProcess = require("node:child_process")
|
||||||
|
|
||||||
|
function getJavaVersionOutput(defaultCommand = "java") {
|
||||||
|
try {
|
||||||
|
const command = childProcess.spawnSync(defaultCommand, ["-version"], {
|
||||||
|
encoding: "utf-8",
|
||||||
|
windowsHide: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (command.error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.stderr || command.stdout || ""
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJavaOutput(output) {
|
||||||
|
if (!output || typeof output !== "string") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = output.match(/version\s+"(?:1\.)?(\d+)/i)
|
||||||
|
return match ? parseInt(match[1], 10) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isJavaCompatible(output, requiredMajor) {
|
||||||
|
return parseJavaOutput(output) === requiredMajor
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getJavaVersionOutput,
|
||||||
|
isJavaCompatible,
|
||||||
|
parseJavaOutput,
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const endpoints = {
|
||||||
|
allRuntimes: process.env.ALL_RUNTIMES || "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json",
|
||||||
|
launcherManifest: process.env.LAUNCHER_MANIFEST || "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEndpoint(key, value) {
|
||||||
|
return endpoints[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEndpoint(key) {
|
||||||
|
return endpoints[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allRuntimes: {
|
||||||
|
set: (value) => {
|
||||||
|
return setEndpoint("allRuntimes", value)
|
||||||
|
},
|
||||||
|
get: () => {
|
||||||
|
return getEndpoint("allRuntimes")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
launcherManifest: {
|
||||||
|
set: (value) => {
|
||||||
|
return setEndpoint("launcherManifest", value)
|
||||||
|
},
|
||||||
|
get: () => {
|
||||||
|
return getEndpoint("launcherManifest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
const fs = require("node:fs")
|
||||||
|
const path = require("node:path")
|
||||||
|
|
||||||
|
function getPlatformKey() {
|
||||||
|
const platform = process.platform
|
||||||
|
const arch = process.arch
|
||||||
|
let candidate = null
|
||||||
|
|
||||||
|
if (platform === "win32") {
|
||||||
|
if (arch === "arm64") {
|
||||||
|
candidate = "windows-arm64"
|
||||||
|
}
|
||||||
|
else if (arch === "x64") {
|
||||||
|
candidate = "windows-x64"
|
||||||
|
}
|
||||||
|
else if (arch === "ia32") {
|
||||||
|
candidate = "windows-x86"
|
||||||
|
}
|
||||||
|
} else if (platform === "darwin") {
|
||||||
|
if (arch === "arm64") {
|
||||||
|
candidate = "mac-os-arm64"
|
||||||
|
} else {
|
||||||
|
candidate = "mac-os"
|
||||||
|
}
|
||||||
|
} else if (platform === "linux") {
|
||||||
|
if (arch === "ia32") {
|
||||||
|
candidate = "linux-i386"
|
||||||
|
} else {
|
||||||
|
candidate = "linux"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets["linux"] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function determineInstallationPath(component, majorVersion, customInstallPath = null) {
|
||||||
|
if (typeof customInstallPath == "string") {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(customInstallPath)
|
||||||
|
return customInstallPath
|
||||||
|
} catch (error) {
|
||||||
|
await fs.promises.mkdir(customInstallPath, { recursive: true })
|
||||||
|
return customInstallPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (process.platform) {
|
||||||
|
case "win32":
|
||||||
|
return path.join("Program files", "Java", component, majorVersion)
|
||||||
|
case "linux":
|
||||||
|
return path.join("opt", "java", component, majorVersion)
|
||||||
|
case "macos":
|
||||||
|
return path.join("Library", "Java", "JavaVirtualMachines", `${component}-${majorVersion}`)
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported OS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getPlatformKey,
|
||||||
|
determineInstallationPath
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
const config = require("./config")
|
||||||
|
|
||||||
|
async function getVersionManifest(version) {
|
||||||
|
const mainifestRequest = await fetch(config.launcherManifest.get())
|
||||||
|
const manifest = await mainifestRequest.json()
|
||||||
|
const component = manifest.versions.find($version => $version.id == version)
|
||||||
|
if (!typeof component == "object") {
|
||||||
|
throw new Error("Unknown version")
|
||||||
|
}
|
||||||
|
return component.url
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRequiredJREFor(versionUrl) {
|
||||||
|
const mainifestRequest = await fetch(versionUrl)
|
||||||
|
const manifest = await mainifestRequest.json()
|
||||||
|
return manifest.javaVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJREComponentDetails(componentRequested, os) {
|
||||||
|
const mainifestRequest = await fetch(config.allRuntimes.get())
|
||||||
|
const manifest = await mainifestRequest.json()
|
||||||
|
const component = manifest[os][componentRequested]
|
||||||
|
if (typeof component != "object") {
|
||||||
|
throw new Error("No JRE Component found")
|
||||||
|
}
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJREComponentManifest(url) {
|
||||||
|
const mainifestRequest = await fetch(url)
|
||||||
|
const manifest = await mainifestRequest.json()
|
||||||
|
return manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getVersionManifest,
|
||||||
|
getRequiredJREFor,
|
||||||
|
getJREComponentDetails,
|
||||||
|
getJREComponentManifest
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user