aboutsummaryrefslogtreecommitdiff
path: root/src/modules/packages.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/packages.js')
-rw-r--r--src/modules/packages.js458
1 files changed, 458 insertions, 0 deletions
diff --git a/src/modules/packages.js b/src/modules/packages.js
new file mode 100644
index 0000000..c9fb1c4
--- /dev/null
+++ b/src/modules/packages.js
@@ -0,0 +1,458 @@
+const path = require("path");
+const fs = require("fs-extra");
+const unzip = require("unzipper");
+const { app, ipcMain } = require("electron");
+const https = require("follow-redirects").https;
+
+const json = require("./json");
+const win = require("./window");
+const settings = require("./settings");
+
+console = require("./console");
+
+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}
+ }
+
+ // only create folder if the profile folder exists
+ if (fs.existsSync(path.dirname(packages.path))) {
+ // 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, no_functions) => {
+ 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"),
+
+ // this will be set later on
+ has_mods: false,
+
+ // contents of `manifest.json` or `false` if it can
+ // be parsed correctly
+ manifest: json(
+ path.join(package_path, "manifest.json")
+ ),
+ }
+
+ // if the package has a `mods` folder, and it's not
+ // empty, then we can assume that the package does
+ // indeed have mods
+ let mods_dir = path.join(package_path, "mods");
+ if (fs.existsSync(mods_dir) &&
+ fs.lstatSync(mods_dir).isDirectory() &&
+ fs.readdirSync(mods_dir).length >= 1) {
+
+ package_list[files[i]].has_mods = true;
+ }
+
+ // add `.remove()` function, mostly just a shorthand,
+ // unless `no_functions` is `true`
+ if (! no_functions) {
+ 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);
+
+ // removes zip's and folders
+ let cleanup = () => {
+ console.info("Cleaning up cache folder of mod:", name);
+ if (zip_path && fs.existsSync(zip_path)) {
+ fs.rm(zip_path, {recursive: true});
+ console.ok("Cleaned archive of mod:", name);
+ }
+
+ if (package_path && fs.existsSync(package_path)) {
+ fs.rm(zip_path, {recursive: true});
+ console.ok("Cleaned mod folder:", name);
+ }
+
+ console.ok("Cleaned up cache folder of mod:", name);
+ }
+
+ console.info("Downloading package:", name);
+ // download `url` to a temporary dir, and return the path to it
+ let zip_path = await packages.download(url, name);
+
+ console.info("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.info("Verifying package:", name);
+ let verification = packages.verify(package_path);
+
+ switch(verification) {
+ case true: break;
+ 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(
+ `${lang("gui.mods.confirm_plugins_title")} ${name} \n\n` +
+ lang("gui.mods.confirm_plugins_description")
+ )
+
+ // check whether the user cancelled or confirmed the
+ // installation, and act accordingly
+ if (! confirmation) {
+ return console.ok("Cancelled package installation:", name);
+ }
+ break;
+ default:
+ ipcMain.emit("failed-mod", name);
+
+ // other unhandled error
+ console.error(
+ "Verification of package failed:", name,
+ ", reason:", verification
+ );
+
+ return cleanup();
+ }
+
+ console.ok("Verified package:", name);
+
+ console.info("Deleting older version(s), if it exists:", name);
+ // check and delete any mod with the name package details in the old
+ // `mods` folder, if there are any at all
+ let mods = require("./mods");
+ let mods_list = mods.list().all;
+ for (let i = 0; i < mods_list.length; i++) {
+ let mod = mods_list[i];
+
+ if (mod.manifest_name == package_name) {
+ mods.remove(mod.name);
+ continue;
+ }
+
+ // normalizes a string, i.e attempt to make two strings
+ // identical, that simply have slightly different formatting, as
+ // an example, these strings:
+ //
+ // "Mod_Name" and "Mod name"
+ //
+ // will just become:
+ //
+ // "modname"
+ let normalize = (string) => {
+ return string.toLowerCase()
+ .replaceAll("_", "")
+ .replaceAll(".", "")
+ .replaceAll(" ", "");
+ }
+
+ // check if the mod's name from it's `mod.json` file when
+ // normalized, is the same as the normalized name of the package
+ if (normalize(mod.name) == normalize(package_name)) {
+ mods.remove(mod.name);
+ continue;
+ }
+
+ // check if the name of the mod's folder when normalized, is the
+ // same as the normalized name of the package
+ if (normalize(mod.folder_name) == normalize(package_name)) {
+ mods.remove(mod.name);
+ continue;
+ }
+ }
+
+ // removes older version of package inside the `packages` folder
+ packages.remove(author, package_name);
+ packages.remove(author, package_name, version);
+
+ console.info("Moving package:", name);
+ let moved = packages.move(package_path);
+
+ if (! moved) {
+ ipcMain.emit("failed-mod", name);
+ console.error("Moving package failed:", name);
+
+ cleanup();
+
+ return false;
+ }
+
+ ipcMain.emit("installed-mod", "", {
+ name: name,
+ fancy_name: package_name
+ })
+
+ console.ok("Installed package:", name);
+ cleanup();
+
+ return true;
+}
+
+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", () => {
+ setInterval(() => {
+ resolve(extract_dir);
+ }, 1000)
+ });
+ })
+}
+
+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 = 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_path) || fs.lstatSync(mods_path).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";
+ }
+
+ // make sure files in the `mods` folder actually are mods, and if
+ // none of them are, then we make sure to return back that are no
+ // mods installed
+ let found_mod = false;
+ let mods = fs.readdirSync(mods_path);
+ for (let i = 0; i < mods.length; i++) {
+ let mod_file = path.join(mods_path, mods[i], "mod.json");
+
+ // make sure mod.json exists, and is a file, otherwise, this
+ // is unlikely to be a mod folder
+ if (! fs.existsSync(mod_file)
+ || ! fs.statSync(mod_file).isFile()) {
+ continue;
+ }
+
+ // attempt to read the mod.json file, and if it succeeds, then
+ // this is likely to be a mod
+ let json_data = json(mod_file);
+ if (json_data) {
+ found_mod = true;
+ }
+ }
+
+ if (! found_mod) {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;