diff options
Diffstat (limited to 'src/modules')
-rw-r--r-- | src/modules/mods.js | 170 | ||||
-rw-r--r-- | src/modules/packages.js | 458 |
2 files changed, 574 insertions, 54 deletions
diff --git a/src/modules/mods.js b/src/modules/mods.js index fc2a682..3c469c3 100644 --- a/src/modules/mods.js +++ b/src/modules/mods.js @@ -55,57 +55,111 @@ mods.list = () => { }; } - let files = fs.readdirSync(mods.path); - files.forEach((file) => { - // return early if `file` isn't a folder - if (! fs.statSync(path.join(mods.path, file)).isDirectory()) { - return; - } + let get_in_dir = (dir, package_obj) => { + let packaged_mods = []; + let files = fs.readdirSync(dir); + + files.forEach((file) => { + // return early if `file` isn't a folder + if (! fs.statSync(path.join(dir, file)).isDirectory()) { + return; + } + + let modjson = path.join(dir, file, "mod.json"); + + // return early if mod.json doesn't exist or isn't a file + if (! fs.existsSync(modjson) || ! fs.statSync(modjson).isFile()) { + return; + } + + let mod = json(modjson); + if (! mod) {return} + + let obj = { + author: mod.Author || false, + version: mod.Version || "unknown", + name: mod.Name || "unknown", + description: mod.Description || "", - let modjson = path.join(mods.path, file, "mod.json"); - - // return early if mod.json doesn't exist or isn't a file - if (! fs.existsSync(modjson) || ! fs.statSync(modjson).isFile()) { + folder_name: file, + folder_path: path.join(dir, file), + + package: package_obj || false + } + + if (obj.package) { + packaged_mods.push(obj.name); + obj.author = obj.package.author; + } + + obj.disabled = ! mods.modfile.get(obj.name); + + // add manifest data from manifest.json, if it exists + let manifest_file = path.join(dir, file, "manifest.json"); + if (fs.existsSync(manifest_file)) { + let manifest = json(manifest_file); + if (manifest != false) { + obj.manifest_name = manifest.name; + if (obj.version == "unknown") { + obj.version = manifest.version_number; + } + } + } + + // add author data from author file, if it exists + let author_file = path.join(dir, file, "thunderstore_author.txt"); + if (fs.existsSync(author_file)) { + obj.author = fs.readFileSync(author_file, "utf8"); + } + + // add mod to their respective disabled or enabled Array + if (obj.disabled) { + disabled.push(obj); + } else { + enabled.push(obj); + } + }) + + if (packaged_mods.length == 0) { return; } - let mod = json(modjson); - if (! mod) {return} - - let obj = { - Author: false, - Version: "unknown", - Name: "unknown", - FolderName: file, - ...mod} - - obj.Disabled = ! mods.modfile.get(obj.Name); - - // add manifest data from manifest.json, if it exists - let manifest_file = path.join(mods.path, file, "manifest.json"); - if (fs.existsSync(manifest_file)) { - let manifest = json(manifest_file); - if (manifest != false) { - obj.ManifestName = manifest.name; - if (obj.Version == "unknown") { - obj.Version = manifest.version_number; + let add_packaged_mods = (mods_array) => { + for (let i = 0; i < mods_array.length; i++) { + if (mods_array[i].package.package_name !== + package_obj.package_name) { + + continue; } + + mods_array[i].packaged_mods = packaged_mods; } - } - // add author data from author file, if it exists - let author_file = path.join(mods.path, file, "thunderstore_author.txt"); - if (fs.existsSync(author_file)) { - obj.Author = fs.readFileSync(author_file, "utf8"); + return mods_array; } - // add mod to their respective disabled or enabled Array - if (obj.Disabled) { - disabled.push(obj); - } else { - enabled.push(obj); + enabled = add_packaged_mods(enabled); + disbled = add_packaged_mods(disabled); + } + + // get mods in `mods` folder + get_in_dir(mods.path); + + // get mods in `packages` folder + let packages = require("./packages"); + let package_list = require("./packages").list(packages.path, true); + for (let i in package_list) { + // make sure the package actually has mods + if (! package_list[i].has_mods) { + continue; } - }) + + // search the package's `mods` folder + get_in_dir( + path.join(package_list[i].package_path, "mods"), + package_list[i] + ) + } return { enabled: enabled, @@ -136,7 +190,7 @@ mods.get = (mod) => { // search for mod in list for (let i = 0; i < list.length; i++) { - if (list[i].Name == mod) { + if (list[i].name == mod) { // found mod, return data return list[i]; } else {continue} @@ -176,7 +230,7 @@ mods.modfile.gen = () => { let list = mods.list().all; // get list of all mods for (let i = 0; i < list.length; i++) { // add every mod to the list - names[list[i].Name] = true + names[list[i].name] = true } // write the actual file @@ -516,35 +570,43 @@ mods.remove = (mod) => { if (mod == "allmods") { let modlist = mods.list().all; for (let i = 0; i < modlist.length; i++) { - mods.remove(modlist[i].Name); + mods.remove(modlist[i].name); } return } - let mod_name = mods.get(mod).FolderName; + let mod_data = mods.get(mod); + let mod_name = mod_data.folder_name; + if (! mod_name) { console.error("error: " + lang("cli.mods.cantfind")); cli.exit(1); return; } - let path_to_mod = path.join(mods.path, mod_name); + let mod_path = mod_data.folder_path; + + // if the mod comes from a package, we'll want to set `mod_path` to + // the package's folder, that way everything gets removed cleanly + if (mod_data.package) { + mod_path = mod_data.package.package_path; + } - // return early if path_to_mod isn't a folder - if (! fs.statSync(path_to_mod).isDirectory()) { + // return early if `mod_path` isn't a folder + if (! fs.statSync(mod_path).isDirectory()) { return cli.exit(1); } - let manifestname = null; + let manifest_name = null; // if the mod has a manifest.json we want to save it now so we can // send it later when telling the renderer about the deleted mod - if (fs.existsSync(path.join(path_to_mod, "manifest.json"))) { - manifestname = require(path.join(path_to_mod, "manifest.json")).name; + if (fs.existsSync(path.join(mod_path, "manifest.json"))) { + manifest_name = json(path.join(mod_path, "manifest.json")).name; } // actually remove the mod itself - fs.rmSync(path_to_mod, {recursive: true}); + fs.rmSync(mod_path, {recursive: true}); console.ok(lang("cli.mods.removed")); cli.exit(); @@ -555,7 +617,7 @@ mods.remove = (mod) => { // relevant info for it to properly update everything graphically ipcMain.emit("removed-mod", "", { name: mod.replace(/^.*(\\|\/|\:)/, ""), - manifestname: manifestname + manifest_name: manifest_name }); } @@ -580,7 +642,7 @@ mods.toggle = (mod, fork) => { if (mod == "allmods") { let modlist = mods.list().all; // get list of all mods for (let i = 0; i < modlist.length; i++) { // run through list - mods.toggle(modlist[i].Name, true); // enable mod + mods.toggle(modlist[i].name, true); // enable mod } console.ok(lang("cli.mods.toggledall")); 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; |