Skip to content

Instantly share code, notes, and snippets.

@VottusCode
Created February 17, 2022 10:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save VottusCode/45915f46bf2291c64a80cbd45203b44f to your computer and use it in GitHub Desktop.
Save VottusCode/45915f46bf2291c64a80cbd45203b44f to your computer and use it in GitHub Desktop.
const {BaseCmdRemote} = require("@remotefull/commander/dist/remote/BaseCmdRemote")
class AptCommandError extends Error {
/**
* Raw command output.
* @type {string[]|null}
*/
rawOut;
/**
* @param {string} message
* @param {string[]|null} rawOut
*/
constructor(message, rawOut = null) {
super(message);
this.name = "AptCommandError";
this.rawOut = rawOut;
}
}
class AptPackageError extends AptCommandError {
/**
* Package name related to the error.
* @type {string}
*/
packageName;
/**
* @param {string} message
* @param {string} packageName
* @param {string[]|null} rawOut
*/
constructor(message, packageName, rawOut = null) {
super(message, rawOut);
this.name = "AptPackageError";
this.packageName = packageName;
}
}
class AptPackageAlreadyInstalledError extends AptPackageError {
/**
* Version of the package that is already installed.
* @type {string}
*/
packageVersion;
/**
*
* @param {string} packageName
* @param {string} packageVersion
* @param {string[]|null} rawOut
*/
constructor(packageName, packageVersion, rawOut = null) {
super(`${packageName} is already installed (${packageVersion}).`, packageName, rawOut);
this.packageVersion = packageVersion;
}
}
class AptUnableToLocatePackageError extends AptPackageError {
/**
* @param {string} packageName
* @param {string[]|null} rawOut
*/
constructor(packageName, rawOut = null) {
super(`Unable to locate package ${packageName}`, packageName, rawOut);
this.name = "AptUnableToLocatePackageError";
}
}
class AptDpkgInterruptedError extends AptCommandError {
/**
* @param {string} message
* @param {string[]|null} rawOut
*/
constructor(message, rawOut) {
super(message, rawOut);
this.name = "AptDpkgInterruptedError";
}
}
class AptManager {
/**
* The command remote used to fetch apt package info.
* @type {BaseCmdRemote}
*/
cmd;
/**
* Locations of required binaries
* @type {{ dpkgQuery: string, apt: string, addAptRepository: string }}
*/
bins;
/**
* @param {BaseCmdRemote} cmd
* @param {{ dpkgQuery?: string, apt?: string, addAptRepository?: string }} bins
*/
constructor(cmd, {
dpkgQuery = "/usr/bin/dpkg-query",
apt = "/usr/bin/apt",
addAptRepository = "/usr/bin/add-apt-repository"
}) {
this.cmd = cmd;
this.bins = {dpkgQuery, apt, addAptRepository}
}
/**
* Checks whether a package is installed.
* @param {string} packageName
* @returns {Promise<boolean>}
*/
async isInstalled(packageName) {
const cmd = `${this.bins.dpkgQuery} --show --showformat='\${db:Status-Status}\\n' ${packageName}`
return (await this.cmd.execute(cmd)).trim() === "installed";
}
/**
*
* @param command
* @param {boolean?} throwAlreadyInstalled
* @returns {Promise<string[]>}
*/
async executeAptCommand(command, throwAlreadyInstalled = false) {
const noPkgRegex = /Unable to locate package (.*)/;
const alreadyInstalledRegex = /(.*) is already the newest version \((.*)\)\./;
const dpkgErrorRegex = /dpkg was interrupted, you must manually run 'dpkg --configure -a' to correct the problem./;
return new Promise(async (resolve, reject) => {
const cmd = `DEBIAN_FRONTEND=noninteractive ${this.bins.apt} -o=Dpkg::Use-Pty=0 ${command} -y 2>> /dev/stdout`;
const stream = await this.cmd.executeWithStream(cmd);
let out = [];
stream.on("data", (chunk) => {
// Make sure that the chunk is a string.
chunk = String(chunk).trim();
const skip = [
"WARNING:",
"apt",
"does not have a stable CLI interface.",
"Use with caution in scripts.",
"WARNING: apt does not have a stable CLI interface. Use with caution in scripts.",
"E",
":"
];
// Empty-lines, new-lines and skip
if (chunk === "\n" || chunk.trim().length < 1 || skip.includes(chunk))
return;
out.push(chunk);
console.log(`out: "${chunk}"`)
let match;
if ((match = chunk.match(noPkgRegex))) {
reject(new AptUnableToLocatePackageError(match[1], out))
}
if (throwAlreadyInstalled && (match = chunk.match(alreadyInstalledRegex))) {
reject(new AptPackageAlreadyInstalledError(match[1], match[2], out))
}
if ((match = chunk.match(dpkgErrorRegex))) {
reject(new AptDpkgInterruptedError(match[0], out));
}
})
stream.on("error", reject)
stream.on("end", () => resolve(out))
})
}
/**
* Installs a package.
*
* @param {string} packageName;
* @param {boolean?} throwAlreadyInstalled
* @return {Promise<string[]>} the apt output
*/
async install(packageName, throwAlreadyInstalled = false) {
return this.executeAptCommand(`install ${packageName} -y`, throwAlreadyInstalled);
}
/**
* Purges a package.
*
* @param {string} packageName
* @return {Promise<string[]>} the apt output
*/
async purge(packageName) {
return this.executeAptCommand(`purge ${packageName} -y`);
}
/**
* Check whether software-properties-common and its dependencies are installed.
* @returns {Promise<void>}
*/
async checkForSpc() {
for (const pkg of ["ca-certificates", "apt-transport-https", "software-properties-common"]) {
if (!(await this.isInstalled(pkg))) {
await this.install(pkg)
}
}
}
/**
* Add a repository using apt-add-repository.
* @param {string} repository
* @param {boolean?} checkForSpc
* @returns {Promise<string>} apt command output
*/
async addRepository(repository, checkForSpc = true) {
if (checkForSpc) await this.checkForSpc();
return await this.cmd.execute(`${this.bins.addAptRepository} ${repository} -y`);
}
/**
* Get all repositories.
* @return {Promise<{ repository: string, branches: string[], rawEntry: string }[]>}
*/
async getRepositories() {
const ex = /deb (.*)/;
return (await this.cmd.execute("find /etc/apt/ -name *.list | xargs cat | grep ^[[:space:]]*deb"))
.split("\n")
.filter((line) => line.trim().length >= 1 && ex.test(line))
.map((raw) => {
/**
* @type {RegExpExecArray}
*/
const match = raw.match(ex);
const arr = match[1].split(" ");
return {
repository: arr.shift(),
branches: arr,
rawEntry: raw
}
})
}
/**
* Executes the "update" command.
* @returns {Promise<string[]>}
*/
async update() {
return await this.executeAptCommand("update -y");
}
}
module.exports = {
AptManager,
AptCommandError,
AptPackageError,
AptPackageAlreadyInstalledError,
AptUnableToLocatePackageError,
AptDpkgInterruptedError
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment