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
|
||||
|
||||
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