diff --git a/package.json b/package.json index e87e98a..36b8681 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "electron-differential-updater": "^4.3.2", "electron-updater": "^6.1.1", "got": "11.8.3", + "human-format": "^1.2.0", "less": "^4.2.0", "lodash": "^4.17.21", "node-7z": "^3.0.0", diff --git a/src/main/index.js b/src/main/index.js index d6ec826..5ac07e0 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,3 +1,24 @@ +import lodash from "lodash" + +global.sendToRenderer = (event, data) => { + function serializeIpc(data) { + const copy = lodash.cloneDeep(data) + + // remove fns + if (!Array.isArray(copy)) { + Object.keys(copy).forEach((key) => { + if (typeof copy[key] === "function") { + delete copy[key] + } + }) + } + + return copy + } + + global.win.webContents.send(event, serializeIpc(data)) +} + import path from "node:path" import { app, shell, BrowserWindow, ipcMain } from "electron" @@ -11,8 +32,8 @@ import pkg from "../../package.json" import setup from "./setup" -import PkgManager from "./pkgManager" -import { readManifest } from "./pkgManager" +import PkgManager from "./pkg_mng" +import { readManifest } from "./utils/readManifest" class ElectronApp { constructor() { diff --git a/src/main/pkgManager.js b/src/main/pkgManager.js deleted file mode 100644 index e622c1d..0000000 --- a/src/main/pkgManager.js +++ /dev/null @@ -1,655 +0,0 @@ -import path from "node:path" -import { pipeline as streamPipeline } from "node:stream/promises" -import ChildProcess from "node:child_process" -import fs from "node:fs" -import os from "node:os" - -import open from "open" -import got from "got" -import { extractFull } from "node-7z" -import { rimraf } from "rimraf" -import lodash from "lodash" - -import pkg from "../../package.json" - -global.OS_USERDATA_PATH = path.resolve( - process.env.APPDATA || - (process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"), -) - -global.RUNTIME_PATH = path.join(global.OS_USERDATA_PATH, "rs-bundler") - -const TMP_PATH = path.resolve(os.tmpdir(), "RS-MCPacks") -const INSTALLERS_PATH = path.join(global.RUNTIME_PATH, "installers") -const MANIFEST_PATH = path.join(global.RUNTIME_PATH, "manifests") - -const RealmDBDefault = { - created_at_version: pkg.version, - installations: [], -} - -function serializeIpc(data) { - const copy = lodash.cloneDeep(data) - - // remove fns - if (!Array.isArray(copy)) { - Object.keys(copy).forEach((key) => { - if (typeof copy[key] === "function") { - delete copy[key] - } - }) - } - - return copy -} - -function sendToRenderer(event, data) { - global.win.webContents.send(event, serializeIpc(data)) -} - -export async function fetchAndCreateModule(manifest) { - console.log(`Fetching ${manifest}...`) - - try { - const response = await got.get(manifest) - const moduleCode = response.body - - const newModule = new module.constructor() - newModule._compile(moduleCode, manifest) - - return newModule - } catch (error) { - console.error(error) - } -} - -export async function readManifest(manifest, { just_read = false } = {}) { - // check if manifest is a directory or a url - const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi - - if (urlRegex.test(manifest)) { - const _module = await fetchAndCreateModule(manifest) - const remoteUrl = lodash.clone(manifest) - - manifest = _module.exports - - manifest.remote_url = remoteUrl - } else { - if (!fs.existsSync(manifest)) { - throw new Error(`Manifest not found: ${manifest}`) - } - - if (!fs.statSync(manifest).isFile()) { - throw new Error(`Manifest is not a file: ${manifest}`) - } - - const manifestFilePath = lodash.clone(manifest) - - manifest = require(manifest) - - if (!just_read) { - // copy manifest - fs.copyFileSync(manifestFilePath, path.resolve(MANIFEST_PATH, path.basename(manifest))) - - manifest.remote_url = manifestFilePath - } - } - - return manifest -} - -export default class PkgManager { - constructor() { - this.initialize() - } - - get realmDbPath() { - return path.join(RUNTIME_PATH, "local_realm.json") - } - - get runtimePath() { - return RUNTIME_PATH - } - - async initialize() { - if (!fs.existsSync(RUNTIME_PATH)) { - fs.mkdirSync(RUNTIME_PATH, { recursive: true }) - } - - if (!fs.existsSync(INSTALLERS_PATH)) { - fs.mkdirSync(INSTALLERS_PATH, { recursive: true }) - } - - if (!fs.existsSync(MANIFEST_PATH)) { - fs.mkdirSync(MANIFEST_PATH, { recursive: true }) - } - - if (!fs.existsSync(TMP_PATH)) { - fs.mkdirSync(TMP_PATH, { recursive: true }) - } - - if (!fs.existsSync(this.realmDbPath)) { - console.log(`Creating default realm db...`, this.realmDbPath) - - await this.writeDb(RealmDBDefault) - } - } - - // DB Operations - async readDb() { - return JSON.parse(await fs.promises.readFile(this.realmDbPath, "utf8")) - } - - async writeDb(data) { - return fs.promises.writeFile(this.realmDbPath, JSON.stringify(data, null, 2)) - } - - async appendInstallation(manifest) { - const db = await this.readDb() - - const prevIndex = db.installations.findIndex((i) => i.id === manifest.id) - - if (prevIndex !== -1) { - db.installations[prevIndex] = manifest - } else { - db.installations.push(manifest) - } - - await this.writeDb(db) - } - - // CRUD Operations - async getInstallations() { - const db = await this.readDb() - - return db.installations - } - - async openBundleFolder(manifest_id) { - const db = await this.readDb() - - const index = db.installations.findIndex((i) => i.id === manifest_id) - - if (index !== -1) { - const manifest = db.installations[index] - - open(manifest.install_path) - } - } - - async initManifest(manifest = {}) { - const packPath = path.resolve(INSTALLERS_PATH, manifest.id) - - const osString = `${os.platform()}-${os.arch()}` - - if (typeof manifest.init === "function") { - const init_result = await manifest.init({ - pack_dir: packPath, - tmp_dir: TMP_PATH, - os_string: osString, - }) - - manifest = { - ...manifest, - ...init_result, - } - - delete manifest.init - } - - return { - ...manifest, - packPath: packPath, - osString: osString, - } - } - - async install(manifest) { - try { - let pendingTasks = [] - - manifest = await readManifest(manifest).catch((error) => { - sendToRenderer("runtime:error", "Cannot fetch this manifest") - - return false - }) - - if (!manifest) { - return false - } - - manifest = await this.initManifest(manifest) - - manifest.status = "installing" - - console.log(`Starting to install ${manifest.pack_name}...`) - console.log(`Installing at >`, manifest.packPath) - - sendToRenderer("new:installation", manifest) - - fs.mkdirSync(manifest.packPath, { recursive: true }) - - await this.appendInstallation(manifest) - - if (typeof manifest.on_install === "function") { - await manifest.on_install({ - manifest: manifest, - pack_dir: manifest.packPath, - tmp_dir: TMP_PATH, - }) - } - - if (typeof manifest.git_clones_steps !== "undefined" && Array.isArray(manifest.git_clones_steps)) { - for await (const step of manifest.git_clones_steps) { - const _path = path.resolve(manifest.packPath, step.path) - - console.log(`Cloning ${step.url}...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Cloning ${step.url}`, - }) - - fs.mkdirSync(_path, { recursive: true }) - - await new Promise((resolve, reject) => { - const process = ChildProcess.exec(`${global.GIT_PATH ?? "git"} clone --recurse-submodules --remote-submodules ${step.url} ${_path}`, { - shell: true, - }) - - process.on("exit", resolve) - process.on("error", reject) - }) - } - } - - if (typeof manifest.http_downloads !== "undefined" && Array.isArray(manifest.http_downloads)) { - for await (const step of manifest.http_downloads) { - let _path = path.resolve(manifest.packPath, step.path ?? ".") - - console.log(`Downloading ${step.url}...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Downloading ${step.url}`, - }) - - if (step.tmp) { - _path = path.resolve(TMP_PATH, String(new Date().getTime())) - } - - fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) - - await streamPipeline( - got.stream(step.url), - fs.createWriteStream(_path) - ) - - if (step.execute) { - pendingTasks.push(async () => { - await new Promise(async (resolve, reject) => { - const process = ChildProcess.execFile(_path, { - shell: true, - }, (error) => { - if (error) { - reject(error) - } else { - resolve() - } - }) - - process.on("exit", resolve) - process.on("error", reject) - }) - }) - } - - if (step.extract) { - console.log(`Extracting ${step.extract}...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Extracting bundle ${step.extract}`, - }) - - await new Promise((resolve, reject) => { - const extract = extractFull(_path, step.extract, { - $bin: global.SEVENZIP_PATH - }) - - extract.on("error", reject) - extract.on("end", resolve) - }) - } - } - } - - if (pendingTasks.length > 0) { - console.log(`Performing pending tasks...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Performing pending tasks...`, - }) - - for await (const task of pendingTasks) { - await task() - } - } - - if (typeof manifest.after_install === "function") { - console.log(`Performing after_install hook...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Performing after_install hook...`, - }) - - await manifest.after_install({ - manifest, - pack_dir: manifest.packPath, - tmp_dir: TMP_PATH - }) - } - - manifest.status = "installed" - manifest.install_path = manifest.packPath - manifest.installed_at = new Date() - manifest.last_update = null - - await this.appendInstallation(manifest) - - console.log(`Successfully installed ${manifest.pack_name}!`) - - sendToRenderer(`installation:done`, { - ...manifest, - statusText: "Successfully installed", - }) - } catch (error) { - manifest.status = "failed" - - sendToRenderer(`installation:error`, { - ...manifest, - statusText: error.toString(), - }) - - console.error(error) - - fs.rmdirSync(manifest.packPath, { recursive: true }) - } - } - - async uninstall(manifest_id) { - console.log(`Uninstalling >`, manifest_id) - - sendToRenderer("installation:status", { - status: "uninstalling", - id: manifest_id, - statusText: `Uninstalling ${manifest_id}...`, - }) - - const db = await this.readDb() - - const manifest = db.installations.find((i) => i.id === manifest_id) - - if (!manifest) { - sendToRenderer("runtime:error", "Manifest not found") - return false - } - - if (manifest.remote_url) { - const remoteManifest = await readManifest(manifest.remote_url, { just_read: true }) - - if (typeof remoteManifest.uninstall === "function") { - console.log(`Performing uninstall hook...`) - - await remoteManifest.uninstall({ - manifest: remoteManifest, - pack_dir: remoteManifest.install_path, - tmp_dir: TMP_PATH, - }) - } - } - - await rimraf(manifest.install_path) - - db.installations = db.installations.filter((i) => i.id !== manifest_id) - - await this.writeDb(db) - - sendToRenderer("installation:uninstalled", { - id: manifest_id, - }) - } - - async update(manifest_id) { - try { - let pendingTasks = [] - - console.log(`Updating >`, manifest_id) - - sendToRenderer("installation:status", { - status: "updating", - id: manifest_id, - statusText: `Updating ${manifest_id}...`, - }) - - const db = await this.readDb() - - let manifest = db.installations.find((i) => i.id === manifest_id) - - if (!manifest) { - sendToRenderer("runtime:error", "Manifest not found") - return false - } - - const packPath = manifest.install_path - - if (manifest.remote_url) { - manifest = await readManifest(manifest.remote_url, { just_read: true }) - } - - manifest.status = "updating" - - manifest = await this.initManifest(manifest) - - if (typeof manifest.update === "function") { - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Performing update hook...`, - }) - - console.log(`Performing update hook...`) - - await manifest.update({ - manifest, - pack_dir: packPath, - tmp_dir: TMP_PATH - }) - } - - if (typeof manifest.git_update !== "undefined" && Array.isArray(manifest.git_update)) { - for await (const step of manifest.git_update) { - const _path = path.resolve(packPath, step.path) - - console.log(`GIT Pulling ${step.url}`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `GIT Pulling ${step.url}`, - }) - - await new Promise((resolve, reject) => { - const process = ChildProcess.exec(`${global.GIT_PATH ?? "git"} pull`, { - cwd: _path, - shell: true, - }) - - process.on("exit", resolve) - process.on("error", reject) - }) - } - } - - if (typeof manifest.http_downloads !== "undefined" && Array.isArray(manifest.http_downloads)) { - for await (const step of manifest.http_downloads) { - let _path = path.resolve(packPath, step.path ?? ".") - - console.log(`Downloading ${step.url}...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Downloading ${step.url}`, - }) - - if (step.tmp) { - _path = path.resolve(TMP_PATH, String(new Date().getTime())) - } - - fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) - - await streamPipeline( - got.stream(step.url), - fs.createWriteStream(_path) - ) - - if (step.execute) { - pendingTasks.push(async () => { - await new Promise(async (resolve, reject) => { - const process = ChildProcess.execFile(_path, { - shell: true, - }, (error) => { - if (error) { - reject(error) - } else { - resolve() - } - }) - - process.on("exit", resolve) - process.on("error", reject) - }) - }) - } - - if (step.extract) { - console.log(`Extracting ${step.extract}...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Extracting bundle ${step.extract}`, - }) - - await new Promise((resolve, reject) => { - const extract = extractFull(_path, step.extract, { - $bin: global.SEVENZIP_PATH - }) - - extract.on("error", reject) - extract.on("end", resolve) - }) - } - } - } - - if (pendingTasks.length > 0) { - console.log(`Performing pending tasks...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Performing pending tasks...`, - }) - - for await (const task of pendingTasks) { - await task() - } - } - - if (typeof manifest.after_install === "function") { - console.log(`Performing after_install hook...`) - - sendToRenderer(`installation:status`, { - ...manifest, - statusText: `Performing after_install hook...`, - }) - - await manifest.after_install({ - manifest, - pack_dir: packPath, - tmp_dir: TMP_PATH - }) - } - - manifest.status = "installed" - manifest.install_path = packPath - manifest.last_update = new Date() - - await this.appendInstallation(manifest) - - console.log(`Successfully updated ${manifest.pack_name}!`) - - sendToRenderer(`installation:done`, { - ...manifest, - statusText: "Successfully updated", - }) - } catch (error) { - manifest.status = "failed" - - sendToRenderer(`installation:error`, { - ...manifest, - statusText: error.toString(), - }) - - console.error(error) - } - } - - async execute(manifest_id) { - console.log(`Executing ${manifest_id}...`) - - sendToRenderer("installation:status", { - status: "starting", - id: manifest_id, - statusText: `Executing ${manifest_id}...`, - }) - - const db = await this.readDb() - - let manifest = db.installations.find((i) => i.id === manifest_id) - - if (!manifest) { - sendToRenderer("runtime:error", "Manifest not found") - return false - } - - if (manifest.remote_url) { - manifest = await readManifest(manifest.remote_url, { just_read: true }) - } - - manifest = await this.initManifest(manifest) - - if (typeof manifest.execute !== "function") { - sendToRenderer("installation:status", { - status: "execution_failed", - ...manifest, - }) - - return false - } - - await manifest.execute({ - manifest, - pack_dir: manifest.packPath, - tmp_dir: TMP_PATH - }) - - sendToRenderer("installation:status", { - status: "installed", - ...manifest, - }) - - console.log(`Successfully executed ${manifest_id}!`) - - return true - } -} \ No newline at end of file diff --git a/src/main/pkg_mng/index.js b/src/main/pkg_mng/index.js new file mode 100644 index 0000000..48c562b --- /dev/null +++ b/src/main/pkg_mng/index.js @@ -0,0 +1,418 @@ +import os from "node:os" + +global.OS_USERDATA_PATH = path.resolve( + process.env.APPDATA || + (process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"), +) +global.RUNTIME_PATH = path.join(global.OS_USERDATA_PATH, "rs-bundler") +global.TMP_PATH = path.resolve(os.tmpdir(), "rs-bundler") +global.INSTALLERS_PATH = path.join(global.RUNTIME_PATH, "installations") +global.MANIFEST_PATH = path.join(global.RUNTIME_PATH, "manifests") + +import path from "node:path" +import fs from "node:fs" + +import open from "open" +import { rimraf } from "rimraf" + +import readManifest from "../utils/readManifest" +import initManifest from "../utils/initManifest" + +import ISM_HTTP from "./installs_steps_methods/http" +import ISM_GIT from "./installs_steps_methods/git" + +import pkg from "../../../package.json" + +const RealmDBDefault = { + created_at_version: pkg.version, + installations: [], +} + +const InstallationStepsMethods = { + http: ISM_HTTP, + git: ISM_GIT, +} + +export default class PkgManager { + constructor() { + this.initialize() + } + + get realmDbPath() { + return path.join(RUNTIME_PATH, "local_realm.json") + } + + get runtimePath() { + return RUNTIME_PATH + } + + async initialize() { + if (!fs.existsSync(RUNTIME_PATH)) { + fs.mkdirSync(RUNTIME_PATH, { recursive: true }) + } + + if (!fs.existsSync(INSTALLERS_PATH)) { + fs.mkdirSync(INSTALLERS_PATH, { recursive: true }) + } + + if (!fs.existsSync(MANIFEST_PATH)) { + fs.mkdirSync(MANIFEST_PATH, { recursive: true }) + } + + if (!fs.existsSync(TMP_PATH)) { + fs.mkdirSync(TMP_PATH, { recursive: true }) + } + + if (!fs.existsSync(this.realmDbPath)) { + console.log(`Creating default realm db...`, this.realmDbPath) + + await this.writeDb(RealmDBDefault) + } + } + + // DB Operations + async readDb() { + return JSON.parse(await fs.promises.readFile(this.realmDbPath, "utf8")) + } + + async writeDb(data) { + return fs.promises.writeFile(this.realmDbPath, JSON.stringify(data, null, 2)) + } + + async appendInstallation(manifest) { + const db = await this.readDb() + + const prevIndex = db.installations.findIndex((i) => i.id === manifest.id) + + if (prevIndex !== -1) { + db.installations[prevIndex] = manifest + } else { + db.installations.push(manifest) + } + + await this.writeDb(db) + } + + // CRUD Operations + async getInstallations() { + const db = await this.readDb() + + return db.installations + } + + async openBundleFolder(manifest_id) { + const db = await this.readDb() + + const index = db.installations.findIndex((i) => i.id === manifest_id) + + if (index !== -1) { + const manifest = db.installations[index] + + open(manifest.install_path) + } + } + + async install(manifest) { + try { + let pendingTasks = [] + + manifest = await readManifest(manifest).catch((error) => { + global.sendToRenderer("runtime:error", "Cannot fetch this manifest") + + return false + }) + + if (!manifest) { + return false + } + + manifest = await initManifest(manifest) + + manifest.status = "installing" + + console.log(`Starting to install ${manifest.pack_name}...`) + console.log(`Installing at >`, manifest.packPath) + + global.sendToRenderer("new:installation", manifest) + + fs.mkdirSync(manifest.packPath, { recursive: true }) + + await this.appendInstallation(manifest) + + if (typeof manifest.on_install === "function") { + await manifest.on_install({ + manifest: manifest, + pack_dir: manifest.packPath, + tmp_dir: TMP_PATH, + }) + } + + if (typeof manifest.git_clones_steps !== "undefined" && Array.isArray(manifest.git_clones_steps)) { + for await (const step of manifest.git_clones_steps) { + await InstallationStepsMethods.git(manifest, step) + } + } + + if (typeof manifest.http_downloads !== "undefined" && Array.isArray(manifest.http_downloads)) { + for await (const step of manifest.http_downloads) { + await InstallationStepsMethods.http(manifest, step) + } + } + + if (pendingTasks.length > 0) { + console.log(`Performing pending tasks...`) + + global.sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Performing pending tasks...`, + }) + + for await (const task of pendingTasks) { + await task() + } + } + + if (typeof manifest.after_install === "function") { + console.log(`Performing after_install hook...`) + + global.sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Performing after_install hook...`, + }) + + await manifest.after_install({ + manifest, + pack_dir: manifest.packPath, + tmp_dir: TMP_PATH + }) + } + + manifest.status = "installed" + manifest.install_path = manifest.packPath + manifest.installed_at = new Date() + manifest.last_update = null + + await this.appendInstallation(manifest) + + console.log(`Successfully installed ${manifest.pack_name}!`) + + global.sendToRenderer(`installation:done`, { + ...manifest, + statusText: "Successfully installed", + }) + } catch (error) { + manifest.status = "failed" + + global.sendToRenderer(`installation:error`, { + ...manifest, + statusText: error.toString(), + }) + + console.error(error) + + fs.rmdirSync(manifest.packPath, { recursive: true }) + } + } + + async uninstall(manifest_id) { + console.log(`Uninstalling >`, manifest_id) + + global.sendToRenderer("installation:status", { + status: "uninstalling", + id: manifest_id, + statusText: `Uninstalling ${manifest_id}...`, + }) + + const db = await this.readDb() + + const manifest = db.installations.find((i) => i.id === manifest_id) + + if (!manifest) { + global.sendToRenderer("runtime:error", "Manifest not found") + return false + } + + if (manifest.remote_url) { + const remoteManifest = await readManifest(manifest.remote_url, { just_read: true }) + + if (typeof remoteManifest.uninstall === "function") { + console.log(`Performing uninstall hook...`) + + await remoteManifest.uninstall({ + manifest: remoteManifest, + pack_dir: remoteManifest.install_path, + tmp_dir: TMP_PATH, + }) + } + } + + await rimraf(manifest.install_path) + + db.installations = db.installations.filter((i) => i.id !== manifest_id) + + await this.writeDb(db) + + global.sendToRenderer("installation:uninstalled", { + id: manifest_id, + }) + } + + async update(manifest_id) { + try { + let pendingTasks = [] + + console.log(`Updating >`, manifest_id) + + global.sendToRenderer("installation:status", { + status: "updating", + id: manifest_id, + statusText: `Updating ${manifest_id}...`, + }) + + const db = await this.readDb() + + let manifest = db.installations.find((i) => i.id === manifest_id) + + if (!manifest) { + global.sendToRenderer("runtime:error", "Manifest not found") + return false + } + + const packPath = manifest.install_path + + if (manifest.remote_url) { + manifest = await readManifest(manifest.remote_url, { just_read: true }) + } + + manifest.status = "updating" + + manifest = await initManifest(manifest) + + if (typeof manifest.update === "function") { + global.sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Performing update hook...`, + }) + + console.log(`Performing update hook...`) + + await manifest.update({ + manifest, + pack_dir: packPath, + tmp_dir: TMP_PATH + }) + } + + if (typeof manifest.git_update !== "undefined" && Array.isArray(manifest.git_update)) { + for await (const step of manifest.git_update) { + await InstallationStepsMethods.git(manifest, step) + } + } + + if (typeof manifest.http_downloads !== "undefined" && Array.isArray(manifest.http_downloads)) { + for await (const step of manifest.http_downloads) { + await InstallationStepsMethods.http(manifest, step) + } + } + + if (pendingTasks.length > 0) { + console.log(`Performing pending tasks...`) + + global.sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Performing pending tasks...`, + }) + + for await (const task of pendingTasks) { + await task() + } + } + + if (typeof manifest.after_update === "function") { + console.log(`Performing after_update hook...`) + + global.sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Performing after_update hook...`, + }) + + await manifest.after_update({ + manifest, + pack_dir: packPath, + tmp_dir: TMP_PATH + }) + } + + manifest.status = "installed" + manifest.install_path = packPath + manifest.last_update = new Date() + + await this.appendInstallation(manifest) + + console.log(`Successfully updated ${manifest.pack_name}!`) + + global.sendToRenderer(`installation:done`, { + ...manifest, + statusText: "Successfully updated", + }) + } catch (error) { + manifest.status = "failed" + + global.sendToRenderer(`installation:error`, { + ...manifest, + statusText: error.toString(), + }) + + console.error(error) + } + } + + async execute(manifest_id) { + console.log(`Executing ${manifest_id}...`) + + global.sendToRenderer("installation:status", { + status: "starting", + id: manifest_id, + statusText: `Executing ${manifest_id}...`, + }) + + const db = await this.readDb() + + let manifest = db.installations.find((i) => i.id === manifest_id) + + if (!manifest) { + global.sendToRenderer("runtime:error", "Manifest not found") + return false + } + + if (manifest.remote_url) { + manifest = await readManifest(manifest.remote_url, { just_read: true }) + } + + manifest = await initManifest(manifest) + + if (typeof manifest.execute !== "function") { + global.sendToRenderer("installation:status", { + status: "execution_failed", + ...manifest, + }) + + return false + } + + await manifest.execute({ + manifest, + pack_dir: manifest.packPath, + tmp_dir: TMP_PATH + }) + + global.sendToRenderer("installation:status", { + status: "installed", + ...manifest, + }) + + console.log(`Successfully executed ${manifest_id}!`) + + return true + } +} \ No newline at end of file diff --git a/src/main/pkg_mng/installs_steps_methods/git.js b/src/main/pkg_mng/installs_steps_methods/git.js new file mode 100644 index 0000000..580b8d6 --- /dev/null +++ b/src/main/pkg_mng/installs_steps_methods/git.js @@ -0,0 +1,25 @@ +import path from "node:path" +import fs from "node:fs" +import ChildProcess from "node:child_process" + +export default async (manifest, step) => { + const _path = path.resolve(manifest.packPath, step.path) + + console.log(`Cloning ${step.url}...`) + + sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Cloning ${step.url}`, + }) + + fs.mkdirSync(_path, { recursive: true }) + + await new Promise((resolve, reject) => { + const process = ChildProcess.exec(`${global.GIT_PATH ?? "git"} clone --recurse-submodules --remote-submodules ${step.url} ${_path}`, { + shell: true, + }) + + process.on("exit", resolve) + process.on("error", reject) + }) +} \ No newline at end of file diff --git a/src/main/pkg_mng/installs_steps_methods/http.js b/src/main/pkg_mng/installs_steps_methods/http.js new file mode 100644 index 0000000..bc45f7b --- /dev/null +++ b/src/main/pkg_mng/installs_steps_methods/http.js @@ -0,0 +1,90 @@ +import path from "node:path" +import fs from "node:fs" +import { pipeline as streamPipeline } from "node:stream/promises" + +import humanFormat from "human-format" + +import got from "got" + +import extractFile from "../../utils/extractFile" + +function convertSize(size) { + return `${humanFormat(size, { + decimals: 2, + })}B` +} + +export default async (manifest, step) => { + let _path = path.resolve(manifest.packPath, step.path ?? ".") + + console.log(`Downloading ${step.url} to ${_path}...`) + + sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Downloading ${step.url}`, + }) + + if (step.tmp) { + _path = path.resolve(TMP_PATH, String(new Date().getTime())) + } + + fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) + + if (step.progress) { + const remoteStream = got.stream(step.url) + const localStream = fs.createWriteStream(_path) + + let progress = null + let lastTransferred = 0 + + sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Starting download...`, + }) + + remoteStream.pipe(localStream) + + remoteStream.on("downloadProgress", (_progress) => { + progress = _progress + }) + + const progressInterval = setInterval(() => { + progress.speed = (progress.transferred - lastTransferred) / 1 + + lastTransferred = progress.transferred + + sendToRenderer(`installation:${manifest.id}:status`, { + ...manifest, + progress: progress, + statusText: `Downloaded ${convertSize(progress.transferred)} / ${convertSize(progress.total)} | ${convertSize(progress.speed)}/s`, + }) + }, 1000) + + await new Promise((resolve, reject) => { + localStream.on("finish", resolve) + localStream.on("error", reject) + }) + + clearInterval(progressInterval) + } else { + await streamPipeline( + got.stream(step.url), + fs.createWriteStream(_path) + ) + } + + if (step.extract) { + if (typeof step.extract === "string") { + step.extract = path.resolve(manifest.packPath, step.extract) + } else { + step.extract = path.resolve(manifest.packPath, ".") + } + + sendToRenderer(`installation:status`, { + ...manifest, + statusText: `Extracting file...`, + }) + + await extractFile(_path, step.extract) + } +} \ No newline at end of file diff --git a/src/main/utils/extractFile.js b/src/main/utils/extractFile.js new file mode 100644 index 0000000..833a45e --- /dev/null +++ b/src/main/utils/extractFile.js @@ -0,0 +1,36 @@ +import fs from "node:fs" +import path from "node:path" +import { pipeline as streamPipeline } from "node:stream/promises" + +import { extractFull } from "node-7z" +import unzipper from "unzipper" + +export async function extractFile(file, dest) { + const ext = path.extname(file) + + console.log(`Extracting ${file} to ${dest}...`) + + switch (ext) { + case ".zip": { + await streamPipeline( + fs.createReadStream(file), + unzipper.Extract({ + path: dest, + }) + ) + break + } + case ".7z": { + await extractFull(file, dest, { + $bin: SEVENZIP_PATH + }) + break + } + default: + throw new Error(`Unsupported file extension: ${ext}`) + } + + return dest +} + +export default extractFile \ No newline at end of file diff --git a/src/main/utils/initManifest.js b/src/main/utils/initManifest.js new file mode 100644 index 0000000..841f03c --- /dev/null +++ b/src/main/utils/initManifest.js @@ -0,0 +1,29 @@ +import path from "node:path" +import os from "node:os" + +export default async (manifest = {}) => { + const packPath = path.resolve(INSTALLERS_PATH, manifest.id) + + const osString = `${os.platform()}-${os.arch()}` + + if (typeof manifest.init === "function") { + const init_result = await manifest.init({ + pack_dir: packPath, + tmp_dir: TMP_PATH, + os_string: osString, + }) + + manifest = { + ...manifest, + ...init_result, + } + + delete manifest.init + } + + return { + ...manifest, + packPath: packPath, + osString: osString, + } +} \ No newline at end of file diff --git a/src/main/utils/readManifest.js b/src/main/utils/readManifest.js new file mode 100644 index 0000000..886cfe5 --- /dev/null +++ b/src/main/utils/readManifest.js @@ -0,0 +1,58 @@ +import path from "node:path" +import fs from "node:fs" + +import lodash from "lodash" +import got from "got" + +export async function fetchAndCreateModule(manifest) { + console.log(`Fetching ${manifest}...`) + + try { + const response = await got.get(manifest) + const moduleCode = response.body + + const newModule = new module.constructor() + newModule._compile(moduleCode, manifest) + + return newModule + } catch (error) { + console.error(error) + } +} + +export async function readManifest(manifest, { just_read = false } = {}) { + // check if manifest is a directory or a url + const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi + + if (urlRegex.test(manifest)) { + const _module = await fetchAndCreateModule(manifest) + const remoteUrl = lodash.clone(manifest) + + manifest = _module.exports + + manifest.remote_url = remoteUrl + } else { + if (!fs.existsSync(manifest)) { + throw new Error(`Manifest not found: ${manifest}`) + } + + if (!fs.statSync(manifest).isFile()) { + throw new Error(`Manifest is not a file: ${manifest}`) + } + + const manifestFilePath = lodash.clone(manifest) + + manifest = require(manifest) + + if (!just_read) { + // copy manifest + fs.copyFileSync(manifestFilePath, path.resolve(MANIFEST_PATH, path.basename(manifest))) + + manifest.remote_url = manifestFilePath + } + } + + return manifest +} + +export default readManifest \ No newline at end of file diff --git a/src/renderer/src/pages/manager/index.jsx b/src/renderer/src/pages/manager/index.jsx index 0e4025b..007f6b8 100644 --- a/src/renderer/src/pages/manager/index.jsx +++ b/src/renderer/src/pages/manager/index.jsx @@ -39,7 +39,7 @@ const NewInstallation = (props) => { } const InstallationItem = (props) => { - const { manifest } = props + const [manifest, setManifest] = React.useState(props.manifest) const isLoading = manifest.status === "installing" || manifest.status === "uninstalling" || manifest.status === "updating" const isInstalled = manifest.status === "installed" @@ -61,6 +61,33 @@ const InstallationItem = (props) => { ipc.exec("bundle:uninstall", manifest.id) } + function handleUpdate(event, data) { + setManifest({ + ...manifest, + ...data, + }) + } + + function renderStatusLine(manifest) { + if (isLoading) { + return manifest.status + } + + return `v${manifest.version}` ?? "N/A" + } + + React.useEffect(() => { + ipc.on(`installation:${manifest.id}:status`, handleUpdate) + + return () => { + ipc.off(`installation:${manifest.id}:status`, handleUpdate) + } + }, []) + + React.useEffect(() => { + setManifest(props.manifest) + }, [props.manifest]) + return
{ - isLoading ? manifest.status : `v${manifest.version}` ?? "N/A" + renderStatusLine(manifest) }
{manifest.statusText ?? "Unknown status"}