for reals bitch
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
import { default as PluginManager } from "./PluginManager";
|
||||
import { Plugin, socketioRPC } from "frontblock-generic/Plugin";
|
||||
import * as Logger from 'log4js';
|
||||
|
||||
Logger.configure({
|
||||
appenders:
|
||||
{
|
||||
"PluginManager/Plugin": { type: 'stdout' },
|
||||
//app: { type: 'file', filename: 'application.log' }
|
||||
},
|
||||
categories:
|
||||
{
|
||||
default: { appenders: [ 'PluginManager/Plugin' ], level: 'debug' }
|
||||
}
|
||||
})
|
||||
const logger = Logger.getLogger("PluginManager/Plugin")
|
||||
|
||||
|
||||
export default class PluginManagerPlugin extends PluginManager implements Plugin{
|
||||
|
||||
constructor(){
|
||||
super()
|
||||
|
||||
this.loadedPlugins[this.name] = {backend: this, frontend: this.loadFrontend(this.name)}
|
||||
}
|
||||
|
||||
exportExtraRPCs(): socketioRPC[] {
|
||||
return [
|
||||
{
|
||||
name: "getAvailablePluginNames",
|
||||
visibility: "private",
|
||||
rpc: async() => { return await this.getAvailablePluginNames() },
|
||||
type: 'call'
|
||||
},
|
||||
{
|
||||
name: "getInstalledPluginNames",
|
||||
visibility: "private",
|
||||
rpc: async() => { return await this.getInstalledPluginNames() },
|
||||
type: 'call'
|
||||
},
|
||||
{
|
||||
name: "getLoadedPluginNames",
|
||||
visibility: "private",
|
||||
rpc: async () => { return this.getLoadedPluginNames() },
|
||||
type: 'call'
|
||||
},{
|
||||
name: "installPlugin",
|
||||
visibility: "private",
|
||||
rpc: async(pluginName:string) => { return await this.installPlugin(pluginName) },
|
||||
type: 'call'
|
||||
},
|
||||
{
|
||||
name: "uninstallPlugin",
|
||||
visibility: "private",
|
||||
rpc: async(pluginName:string) => { return await this.uninstallPlugin(pluginName) },
|
||||
type: 'call'
|
||||
},
|
||||
{
|
||||
name: "loadPlugin",
|
||||
visibility: "private",
|
||||
rpc: async(pluginName:string) => { return await this.loadPlugin(pluginName) },
|
||||
type: 'call'
|
||||
},
|
||||
{
|
||||
name: "unloadPlugin",
|
||||
visibility: "private",
|
||||
rpc: async(pluginName:string) => { return this.unloadPlugin(pluginName) },
|
||||
type: 'call'
|
||||
},{
|
||||
name: "extractPlugin",
|
||||
visibility: "private",
|
||||
rpc: async(conf) => { return this.extractPlugin(conf) },
|
||||
type: 'call'
|
||||
},{
|
||||
name: "downloadPlugin",
|
||||
visibility: "private",
|
||||
rpc: async(conf) => { return this.downloadPlugin(conf) },
|
||||
type: 'call'
|
||||
},{
|
||||
name: "getConfig",
|
||||
visibility: "private",
|
||||
rpc: async() => { return this.conf },
|
||||
type: 'call'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await Promise.all(this.getInstalledPluginNames().map(name => {
|
||||
if(name == this.name) return
|
||||
return this.loadPlugin(name)
|
||||
}));
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
import * as Logger from 'log4js';
|
||||
import fetch = require('node-fetch');
|
||||
import fs = require('fs');
|
||||
import path = require('path')
|
||||
import unzip = require('unzip')
|
||||
import easyunzip = require('easy-unzip')
|
||||
|
||||
import { Plugin, ConfigLoader, PluginConfigLoader } from 'frontblock-generic/Plugin';
|
||||
import { SemVer, parse } from "semver";
|
||||
import { once } from 'events';
|
||||
|
||||
Logger.configure({
|
||||
appenders:
|
||||
{
|
||||
"PluginManager": { type: 'stdout' },
|
||||
//app: { type: 'file', filename: 'application.log' }
|
||||
},
|
||||
categories:
|
||||
{
|
||||
default: { appenders: [ 'PluginManager' ], level: 'debug' }
|
||||
}
|
||||
})
|
||||
const logger = Logger.getLogger("PluginManager")
|
||||
const AdmZip = require('adm-zip');
|
||||
|
||||
type PluginVersion = {
|
||||
installed?: SemVer,
|
||||
cached?: SemVer,
|
||||
}
|
||||
export type PluginVersioning = {
|
||||
[pluginName in string]?: PluginVersion | string
|
||||
}
|
||||
export type PluginMap = {[pluginName:string]: {backend: Plugin, frontend:any}}
|
||||
|
||||
export type PluginManagerConfig = {
|
||||
resourceLocation: string,
|
||||
installDir:string
|
||||
}
|
||||
|
||||
export default abstract class PluginManager extends PluginConfigLoader<PluginManagerConfig&PluginVersioning>{
|
||||
|
||||
private cacheDir: string
|
||||
protected loadedPlugins: PluginMap = {}
|
||||
|
||||
constructor(){
|
||||
super("PluginManager")
|
||||
this.cacheDir = path.join(this.conf.installDir, '.cache')
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
getDefaultConfig(): PluginManagerConfig&PluginVersioning{
|
||||
return {resourceLocation: "https://gitea.frontblock.me/api/v1/repos/fb-vendor/", installDir: "plugins"}
|
||||
}
|
||||
|
||||
|
||||
private initialize() {
|
||||
logger.info('Initializing manager')
|
||||
|
||||
try {
|
||||
fs.accessSync(this.cacheDir)
|
||||
logger.info('Manager already installed at ' + this.conf.installDir)
|
||||
} catch (error) {
|
||||
fs.mkdirSync(this.cacheDir, {recursive: true})
|
||||
logger.info('Manager installed to ' + this.cacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async getAvailablePluginNames(): Promise<string[]> {
|
||||
//TODO query api
|
||||
return [
|
||||
'apiclient',
|
||||
'paymentmanager',
|
||||
'wallet',
|
||||
'htmlsupplier'
|
||||
]
|
||||
}
|
||||
|
||||
public getInstalledPluginNames():string[] {
|
||||
try {
|
||||
const installed = fs.readdirSync(this.conf.installDir)
|
||||
return installed.filter( entry => entry[0] != '.')
|
||||
} catch (error) {
|
||||
logger.error("getInstalledPlugins", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public getLoadedPluginNames(): string[] {
|
||||
return Object.keys(this.loadedPlugins)
|
||||
}
|
||||
|
||||
public getLoadedPlugins(): PluginMap {
|
||||
//logger.debug(this.loadedPlugins)
|
||||
return this.loadedPlugins
|
||||
}
|
||||
|
||||
|
||||
private async downloadFile(url: URL, dest: string) {
|
||||
try {
|
||||
const file = fs.createWriteStream(dest)
|
||||
file.on('error', err => {throw new Error(err)})
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error(response.statusText)
|
||||
response.body.pipe(file)
|
||||
await once(file, 'finish');
|
||||
|
||||
} catch (error) {
|
||||
logger.error("downloadFile", error)
|
||||
await fs.promises.unlink(dest)
|
||||
}
|
||||
}
|
||||
|
||||
private async getLatestRelease(pluginName:string):Promise<any>{
|
||||
const response = await fetch(this.conf.resourceLocation + pluginName + '/releases')
|
||||
if (!response.ok) throw new Error(pluginName+": "+response.statusText)
|
||||
|
||||
const releases = await response.json()
|
||||
if (!releases || !releases.length) throw new Error('Empty response')
|
||||
|
||||
return releases[0]
|
||||
}
|
||||
|
||||
private async getLatestVersion(pluginName:string):Promise<SemVer>{
|
||||
const latest = await this.getLatestRelease(pluginName)
|
||||
const version = latest.tag_name
|
||||
return parse(version)!
|
||||
}
|
||||
|
||||
async downloadPlugin(pluginName: string) {
|
||||
try {
|
||||
let version = await this.getLatestVersion(pluginName)
|
||||
logger.info(version)
|
||||
if(this.conf[pluginName] != null && version === this.conf[pluginName]!["installed"].raw){
|
||||
logger.info(pluginName, version, "already installed. skipping", this.conf[pluginName]!["installed"])
|
||||
return
|
||||
}
|
||||
|
||||
const latest = await this.getLatestRelease(pluginName)
|
||||
const archiveURL = latest.assets.filter(asset => asset.name === 'plugin.zip')[0].browser_download_url
|
||||
const checksumURL = latest.assets.filter(asset => asset.name === 'md5sum.txt')[0].browser_download_url
|
||||
if (!version || ! archiveURL || !checksumURL) throw new Error('Malformed response')
|
||||
|
||||
fs.mkdirSync(path.join(this.cacheDir, pluginName), {recursive: true})
|
||||
|
||||
await this.downloadFile(archiveURL, path.join(this.cacheDir, pluginName, 'plugin.zip'))
|
||||
await this.downloadFile(checksumURL, path.join(this.cacheDir, pluginName, 'md5sum.txt'))
|
||||
|
||||
if(this.conf[pluginName] == null){
|
||||
this.conf[pluginName] = {}
|
||||
}
|
||||
this.conf[pluginName]!["cached"] = version
|
||||
this.setConfig(this.conf)
|
||||
|
||||
|
||||
} catch (error) {
|
||||
logger.error("downloadPlugin", error)
|
||||
}
|
||||
}
|
||||
|
||||
extractPlugin(pluginName: string){
|
||||
const archivePath = path.join(this.cacheDir, pluginName, "plugin.zip")
|
||||
const outputPath = path.join(this.conf.installDir,pluginName)
|
||||
//fs.createReadStream(archivePath).pipe(unzip.Extract({ path: outputPath }));
|
||||
var zip = new AdmZip(archivePath);
|
||||
fs.mkdirSync(outputPath, {recursive: true})
|
||||
zip.extractAllTo(outputPath, true);
|
||||
|
||||
if(this.conf[pluginName] == null){
|
||||
this.conf[pluginName] = {}
|
||||
}
|
||||
this.conf[pluginName]!["installed"] = this.conf[pluginName]!["cached"]
|
||||
this.setConfig(this.conf)
|
||||
}
|
||||
|
||||
public async installPlugin(pluginName: string) {
|
||||
try {
|
||||
let version = await this.getLatestVersion(pluginName)
|
||||
|
||||
if(this.conf[pluginName] == null || version != this.conf[pluginName]!["installed"].raw){
|
||||
await this.downloadPlugin(pluginName)
|
||||
this.extractPlugin(pluginName)
|
||||
}
|
||||
await this.loadPlugin(pluginName)
|
||||
} catch (error) {
|
||||
logger.error("installPlugin", error)
|
||||
}
|
||||
}
|
||||
|
||||
public getFrontend(pluginName: string):string{
|
||||
return this.loadedPlugins[pluginName].frontend.toString()
|
||||
}
|
||||
|
||||
public async uninstallPlugin(pluginName: string) {
|
||||
//TODO check for exists
|
||||
logger.info("Deleting plugin", pluginName)
|
||||
await fs.promises.rmdir(path.join(this.conf.installDir,pluginName))
|
||||
}
|
||||
|
||||
|
||||
public async loadPlugin(pluginName:string) {
|
||||
const pth = path.join("..", "..", this.conf.installDir, pluginName)
|
||||
logger.info("Loading plugin from fs", pth+"/Plugin")
|
||||
const clazz = await import(pth+"/Plugin")
|
||||
logger.warn(clazz)
|
||||
let obj:Plugin = new clazz.default()
|
||||
await obj.start()
|
||||
this.loadedPlugins[pluginName] = {backend: obj, frontend: this.loadFrontend(pluginName)}
|
||||
}
|
||||
|
||||
public loadFrontend(pluginName:string){
|
||||
const pth = path.join("..", "..", this.conf.installDir, pluginName)
|
||||
logger.info("Loading frontend plugin from fs", path.join(__dirname,pth,"FrontendPlugin.js"))
|
||||
return fs.readFileSync(path.join(__dirname, pth,"FrontendPlugin.js")).toString()
|
||||
}
|
||||
|
||||
|
||||
public unloadPlugin(pluginName:string):void {
|
||||
if(this.loadedPlugins[pluginName] == null){
|
||||
logger.warn("Cannt unload plugin: Unknown:", pluginName)
|
||||
return
|
||||
}
|
||||
logger.info("Unloading plugin ", pluginName)
|
||||
this.loadedPlugins[pluginName].backend.stop()
|
||||
delete this.loadedPlugins[pluginName]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { PluginModule } from './widget/module';
|
||||
@@ -1,28 +0,0 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
|
||||
export default {
|
||||
input: 'src/frontend/widget/main.ts',
|
||||
output: {
|
||||
file: 'FrontendPlugin.js',
|
||||
format: 'system'
|
||||
},
|
||||
plugins: [
|
||||
resolve({
|
||||
// pass custom options to the resolve plugin
|
||||
customResolveOptions: {
|
||||
moduleDirectory: 'node_modules'
|
||||
}
|
||||
}),
|
||||
typescript({
|
||||
typescript: require('typescript'),
|
||||
tsconfig: "tsconfig.frontend.json"
|
||||
})
|
||||
],
|
||||
external: [
|
||||
'plugins-core',
|
||||
'@angular/core',
|
||||
'@angular/common',
|
||||
'@angular/router'
|
||||
]
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SidebarEntry } from 'frontblock-generic/Plugin';
|
||||
declare const fb
|
||||
@Component({
|
||||
selector: 'pluginmanager', //!!!!
|
||||
template: `
|
||||
<div class="clr-row">
|
||||
<div class="clr-col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
DEBUG
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<div class="card-title">
|
||||
SETUP
|
||||
</div>
|
||||
<div class="card-text">
|
||||
<p>
|
||||
Press this button to install all plugins.
|
||||
<br>
|
||||
Once the installation is done the page will refresh automatically
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-warning" onclick="setup()">SETUP</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class PluginComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
}
|
||||
|
||||
export const sidebarEntry: SidebarEntry = {
|
||||
icon: "wrench",
|
||||
route: "dev/pluginmanager",
|
||||
text: "DEV Plugin manager",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { PluginComponent } from './component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { FrontendPlugin, SidebarEntry, SidebarEntries } from 'frontblock-generic/Plugin';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forChild([
|
||||
{path: "debug", component: PluginComponent}
|
||||
]),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
declarations: [
|
||||
PluginComponent,
|
||||
// ANestedComponent,
|
||||
],
|
||||
entryComponents: [PluginComponent],
|
||||
providers: [{
|
||||
provide: 'provider',
|
||||
useValue: PluginComponent
|
||||
}]
|
||||
})
|
||||
export class PluginModule implements FrontendPlugin{
|
||||
getSidebarEntry(): SidebarEntries {
|
||||
return {
|
||||
icon: "wrench",
|
||||
parentRoute: "pluginmanager",
|
||||
text: "Plugin manager",
|
||||
links: [{
|
||||
route: "debug",
|
||||
text: "DEBUG"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user