import * as Logger from 'log4js' import * as path from "path"; import { promises as fs } from "fs" import * as git from "simple-git/promise" import * as trash from "trash" Logger.configure({ appenders: { "admin/extension": { type: 'stdout' }, //app: { type: 'file', filename: 'application.log' } }, categories: { default: { appenders: [ 'admin/extension' ], level: 'debug' } } }) const exec = require('child-process-promise').exec; const logger = Logger.getLogger("admin/updatemanager") type SharedStatus = { name: string installed?: boolean outdated?: boolean } abstract class Extension{ constructor( protected name: string ){} /** * the generic Status object */ protected async status():Promise{ const installed = await this.isInstalled() const outdated = await this.isOutdated() return { name: this.name, installed: installed, outdated: outdated } } /** * true => extension was installed * false => extension was not install * undefined => signals error */ abstract async install():Promise /** * reverts the install procedure */ abstract async uninstall():Promise /** * true => an update was performed * false => no update was performed * undefined => signals error */ abstract async update(force?: boolean): Promise /** * true => update would install a newer version * false => nothing would be updated if update would be performed * undefined => signals error */ abstract async isOutdated():Promise /** * promises a boolean * true => install would do nothing would it be called * false => install would try to install the newest extension * undefined => signals error */ abstract async isInstalled():Promise } export type FSStatus = SharedStatus & { prefix: string empty?: boolean } export abstract class FSExtension extends Extension{ constructor( name: string, protected readonly prefix: string){ super(name) } public async status(): Promise{ const shdStatus = await super.status() const empty = await this.isEmpty() return {...shdStatus, prefix: this.prefix, empty: empty} } public async install():Promise{ const installed = await this.isInstalled() if (!installed) { logger.info('Installing extension to', path.resolve(this.prefix, this.name)) await fs.mkdir(path.resolve(this.prefix, this.name), {recursive: true}) return true } logger.warn('Extension already installed at', path.resolve(this.prefix, this.name)) return false } public async uninstall(): Promise{ logger.info('Uninstalling extension from', path.resolve(this.prefix, this.name)) return await trash(path.resolve(this.prefix, this.name)) } public async update(force?: boolean): Promise{ if (force) await this.uninstall() return await this.install() } public async isEmpty(): Promise{ if (!this.isInstalled()) return true try { const files = await fs.readdir(path.resolve(this.prefix, this.name)) return files.length === 0 } catch (error) { logger.warn('TODO') return } } public async isInstalled(): Promise{ try { await fs.access(path.resolve(this.prefix, this.name)) return true } catch (error) { return false } } } type NPMStatus = FSStatus & { latest?: string, current?: string, author?: string } export class NPMExtension extends FSExtension{ constructor( pkgName: string, prefix: string){ super(pkgName, prefix) } public async status(): Promise { const fsStatus = await super.status() const latest = await this.getLatestVersion() const author = await this.getAuthor() const current = await this.getCurrentVersion() return { ...fsStatus, latest: latest, author: author, current: current } } public async install(): Promise { super.install() logger.info("npm install", this.name, 'to', path.resolve(this.prefix, this.name)) const { error, stdout, stderr } = await exec('npm install --prefix '+ this.prefix + ' ' + this.name) if (error !== null) { logger.warn('npm install', this.name, 'from prefix', this.prefix, 'stderr:', stderr) return false } else { logger.info('npm install', this.name, 'from prefix', this.prefix, 'stdout:', stdout) return true } } public async uninstall(force?: boolean): Promise { super.uninstall() logger.info("npm uninstall", this.name, 'from', path.resolve(this.prefix, this.name)) const { error, stdout, stderr } = await exec('npm uninstall --prefix '+ this.prefix + ' ' + this.name) if (error) { logger.warn('npm uninstall', this.name, 'from prefix', this.prefix, 'stderr:', stderr) } else { logger.info('npm uninstall', this.name, 'from prefix', this.prefix, 'stdout:', stdout) } } public async update(force?: boolean): Promise { super.update(force) logger.info("npm update", path.resolve(this.prefix, this.name)) const { error, stdout, stderr } = await exec('npm update --prefix '+ this.prefix + ' ' + this.name) if (error) { logger.warn('npm update', this.name, 'from prefix', this.prefix, 'stderr:', stderr) return false } else { logger.info('npm update', this.name, 'from prefix', this.prefix, 'stdout:', stdout) return true } } public async isOutdated(): Promise { try { const { error, stdout, stderr } = await exec('npm outdated --prefix ' + this.prefix + " --json" + ' ' + this.name) if (error) throw new Error(stderr) if (Object.keys(JSON.parse(stdout)).length > 0) return true else return false } catch (error) { logger.warn('npm outdated', this.name, 'from prefix', this.prefix, 'stderr:', error) return } } private async getAuthor(): Promise{ const { error, stdout, stderr } = await exec('npm author ls --prefix ' + this.prefix + ' ' + this.name) if(error){ logger.warn(stderr) return } else { logger.info(stdout) return stdout } } private async getLatestVersion(): Promise{ const { error, stdout, stderr } = await exec('npm view --prefix ' + this.prefix + ' ' + this.name + " version") if(error){ logger.warn(stderr) return } else { logger.info(stdout) return stdout } } private async getCurrentVersion(): Promise{ return //TODO } } export type GitStatus = FSStatus & { branch?: string remote?: string latest?: string } export class GitExtension extends FSExtension{ protected repo: git.SimpleGit constructor( repoName:string, localPrefix:string, protected gitCloneURL: string, protected credentials?: [string, string] ){ super(repoName, localPrefix) } public async status(): Promise{ const fsStatus = await super.status() return { ...fsStatus, branch: 'wat', remote: this.gitCloneURL, latest: 'kek' } } public async install(): Promise { if (super.install()){ if (this.credentials){ } else { } } else { } } public async update(): Promise { return true // do git pull remote origin && git checkout } public async isOutdated(): Promise { return true } } export type PluginStatus = GitStatus & { hasFrontend: boolean, hasBackend: boolean, isFrontendLoaded: boolean, isBackendLoaded: boolean } export class PluginExtension extends GitExtension { protected frontendLoaded: boolean protected backendLoaded: boolean constructor( pluginName: string, gitCloneURL: string, localPrefix: string = './plugins', credentials?: [string, string], protected hasBackend: boolean = true, protected hasFrontend: boolean = true){ super(pluginName,localPrefix,gitCloneURL,credentials) } public async load(): Promise{ if (this.hasFrontend) await this.loadFrontend() if (this.hasBackend) await this.loadBackend() } public async loadFrontend(): Promise{ return //TODO } public async loadBackend(): Promise{ return //TODO } public async unload(): Promise{ if (this.hasFrontend) await this.unloadFrontend() if (this.hasBackend) await this.unloadBackend() } public async unloadFrontend(): Promise{ return //TODO } public async unloadBackend(): Promise{ return //TODO } }