From d61a66893f6212f18da1eba3d874cf23594fa75e Mon Sep 17 00:00:00 2001 From: 0neGal Date: Sun, 16 Jul 2023 17:42:36 +0200 Subject: initial support for packages dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is missing a lot of things, notably support for detecting and installing dependencies, it also is actually being used by the frontend, that still uses `mods.js`, this'll change soon™️ It should also when done, be capable of removing mods installed in the old `mods/` folder when updating those mods, and then installing in the new `packages/` folder, making the upgrade a smooth ride. --- src/modules/packages.js | 338 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 src/modules/packages.js (limited to 'src/modules/packages.js') diff --git a/src/modules/packages.js b/src/modules/packages.js new file mode 100644 index 0000000..9e5b08f --- /dev/null +++ b/src/modules/packages.js @@ -0,0 +1,338 @@ +const path = require("path"); +const fs = require("fs-extra"); +const unzip = require("unzipper"); +const app = require("electron").app; +const https = require("follow-redirects").https; + +const json = require("./json"); +const win = require("./window"); +const settings = require("./settings"); + +var packages = {}; + +function update_path() { + packages.path = path.join(settings.gamepath, "R2Northstar/packages"); + + // make sure the `packages` folder exists + if (fs.existsSync(packages.path)) { + // if it does, but it's a file, remove it + if (fs.lstatSync(packages.path).isFile()) { + fs.rmSync(packages.path); + } else {return} + } + + // create the folder, in case it doesn't already exist + fs.mkdirSync(packages.path); +}; update_path(); + +packages.format_name = (author, package_name, version) => { + return author + "-" + package_name + "-" + version; +} + +// splits the package name into it's individual parts +packages.split_name = (name) => { + let split = name.split("-"); + + // make sure there are only 3 parts + if (split.length !== 3) { + return false; + } + + // return parts + return { + author: split[0], + version: split[2], + package_name: split[1] + } +} + +packages.list = (dir = packages.path) => { + let files = fs.readdirSync(dir); + let package_list = {}; + + for (let i = 0; i < files.length; i++) { + let package_path = path.join(dir, files[i]); + let verification = packages.verify(package_path); + + let split_name = packages.split_name(files[i]); + + if (! split_name) {continue} + + // make sure the package is actually package + switch(verification) { + case true: + case "has-plugins": + package_list[files[i]] = { + // adds `author`, `package_name` and `version` + ...split_name, + + icon: false, // will be set later + package_path: package_path, // path to package + + // this is whether or not the package has plugins + has_plugins: (verification == "has-plugins"), + + // contents of `manifest.json` or `false` if it can + // be parsed correctly + manifest: json( + path.join(package_path, "manifest.json") + ), + } + + // add `.remove()` function, mostly just a shorthand + package_list[files[i]].remove = () => { + return packages.remove( + split_name.author, + split_name.package_name, + split_name.version, + ) + } + + // set the `.icon` property + let icon_file = path.join(package_path, "icon.png"); + if (fs.existsSync(icon_file) && + fs.lstatSync(icon_file).isFile()) { + + package_list[files[i]].icon = icon_file; + } + break; + } + } + + return package_list; +} + +packages.remove = (author, package_name, version) => { + // if `version` is not set, we'll search for a package with the same + // `author` and `package_name` and use the version from that, + // this'll be useful when updating, of course this assumes that + // nobody has two versions of the same package installed + // + // TODO: perhaps we should remove duplicate packages? + if (! version) { + // get list of packages + let list = packages.list(); + + // iterate through them + for (let i in list) { + // check for `author` and `package_name` being the same + if (list[i].author == author && + list[i].package_name == package_name) { + + // set `version` to the found package + version = list[i].version; + break; + } + } + } + + let name = packages.format_name(author, package_name, version); + let package_path = path.join(packages.path, name); + + // make sure the package even exists to begin with + if (! fs.existsSync(package_path)) { + return false; + } + + fs.rmSync(package_path, {recursive: true}); + + // return the inverse of whether the package still exists, this'll + // be equivalent to whether or not the removal was successful + return !! fs.existsSync(package_path); +} + +packages.install = async (url, author, package_name, version) => { + update_path(); + + let name = packages.format_name(author, package_name, version); + + console.log("Downloading package:", name); + // download `url` to a temporary dir, and return the path to it + let zip_path = await packages.download(url, name); + + console.log("Extracting package:", name); + // extract the zip file we downloaded before, and return the path of + // the folder that we extracted it to + let package_path = await packages.extract(zip_path, name); + + + console.log("Verifying package:", name); + let verification = packages.verify(package_path); + + switch(verification) { + case "has-plugins": + // if the package has plugins, then we want to prompt the + // user, and make absolutely certain that they do want to + // install this package, as plugins have security concerns + let confirmation = await win.confirm( + `The following package has native plugins: ${name} \n\n` + + + + + "Native plugins have far more system access than a " + + "regular mod, and because of this they're inherently " + + "less secure to have installed, as a malicious plugin" + + " could do far more harm this way. If this plugin is " + + "one from a trusted developer or similar or you know " + + "what you're doing, then you can disregard this " + + "message completely." + ) + + // check whether the user cancelled or confirmed the + // installation, and act accordingly + if (! confirmation) { + return console.log("Cancelled package installation:", name); + } + break; + default: + // other unhandled error + return console.log( + "Verification of package failed:", name, + ", reason:", verification + ); + } + + console.log("Verified package:", name); + + console.log("Deleting older version, if it exists:", name); + packages.remove(author, package_name, version); + + console.log("Moving package:", name); + let moved = packages.move(package_path); + + if (moved) { + console.log("Installed package:", name); + } else { + console.log("Moving package failed:", name); + } +} + +packages.download = async (url, name) => { + update_path(); + + return new Promise((resolve) => { + // download mod to a temporary location + https.get(url, (res) => { + let tmp = path.join(app.getPath("cache"), "vipertmp"); + + let zip_name = name || "package"; + let zip_path = path.join(tmp, `/${zip_name}.zip`); + + // make sure the temporary folder exists + if (fs.existsSync(tmp)) { + // if it's not a folder, then delete it + if (! fs.statSync(tmp).isDirectory()) { + fs.rmSync(tmp); + } + } else { + // create the folder + fs.mkdirSync(tmp); + + // if there's already a zip file at `zip_path`, then we + // simple remove it, otherwise problems will occur + if (fs.existsSync(zip_path)) { + fs.rmSync(zip_path); + } + } + + // write out the file to the temporary location + let stream = fs.createWriteStream(zip_path); + res.pipe(stream); + + stream.on("finish", () => { + stream.close(); + + // return the path of the downloaded zip file + resolve(zip_path); + }) + }) + }) +} + +packages.extract = async (zip_path, name) => { + // this is where everything from `zip_path` will be extracted + let extract_dir = path.join(path.dirname(zip_path), name); + + // delete `extract_dir` if it does exist + if (fs.existsSync(extract_dir)) { + fs.rmSync(extract_dir, {recursive: true}); + } + + // make an empty folder at `extract_dir` + fs.mkdirSync(extract_dir); + + return new Promise((resolve) => { + fs.createReadStream(zip_path).pipe( + unzip.Extract({ + path: extract_dir + } + )).on("finish", () => { + resolve(extract_dir); + }); + }) +} + +packages.verify = (package_path) => { + // make sure `package_path` is even exists + if (! fs.existsSync(package_path)) { + return "does-not-exist"; + } + + // make sure `package_path` is not a folder + if (fs.lstatSync(package_path).isFile()) { + return "is-file"; + } + + // make sure a manifest file exists, this is required for + // Thunderstore packages, and is therefore also assumed to be + // required here + let manifest = path.join(package_path, "manifest.json"); + if (! fs.existsSync(manifest) || + fs.lstatSync(manifest).isDirectory()) { + + return "missing-manifest"; + } + + // check if there are any plugins in the package + let mods = path.join(package_path, "mods"); + let plugins = path.join(package_path, "plugins"); + if (fs.existsSync(plugins) && fs.lstatSync(plugins).isDirectory()) { + // package has plugins, the function calling `packages.verify()` + // will have to handle this at their own discretion + return "has-plugins"; + } else if (! fs.existsSync(mods) || fs.lstatSync(mods).isFile()) { + // if there are no plugins, then we check if there are any mods, + // if not, then it means there are both no plugins and mods, so + // we signal that back + return "no-mods"; + } + + // all files exist, and everything is just fine + return true; +} + +// moves `package_path` to the packages folder +packages.move = (package_path) => { + update_path(); + + // make sure we're actually dealing with a real folder + if (! fs.existsSync(package_path) || + ! fs.lstatSync(package_path).isDirectory()) { + + return false; + } + + // get path to the package's destination + let new_path = path.join( + packages.path, path.basename(package_path) + ) + + // attempt to move `package_path` to the packages folder + try { + fs.moveSync(package_path, new_path); + }catch(err) {return false} + + return true; +} + +module.exports = packages; -- cgit v1.2.3