Repository management
This commit is contained in:
parent
8809380c86
commit
b4607446b4
94
package-lock.json
generated
94
package-lock.json
generated
@ -45,12 +45,36 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/minipass": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-2.2.0.tgz",
|
||||
"integrity": "sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz",
|
||||
"integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ=="
|
||||
},
|
||||
"@types/tar": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/tar/-/tar-4.0.4.tgz",
|
||||
"integrity": "sha512-0Xv+xcmkTsOZdIF4yCnd7RkOOyfyqPaqJ7RZFKnwdxfDbkN3eAAE9sHl8zJFqBz4VhxolW9EErbjR1oyH7jK2A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/minipass": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
@ -107,6 +131,11 @@
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@ -168,6 +197,14 @@
|
||||
"universalify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"requires": {
|
||||
"minipass": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@ -286,6 +323,23 @@
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
|
||||
"integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"requires": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
@ -327,10 +381,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
@ -347,6 +400,26 @@
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz",
|
||||
"integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==",
|
||||
"requires": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^3.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
@ -372,6 +445,14 @@
|
||||
"semver": "^5.3.0",
|
||||
"tslib": "^1.13.0",
|
||||
"tsutils": "^2.29.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"tsutils": {
|
||||
@ -399,6 +480,11 @@
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,11 +20,15 @@
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/fs-extra": "^9.0.4",
|
||||
"@types/node": "^14.14.9",
|
||||
"@types/tar": "^4.0.4",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/semver": "^7.3.4",
|
||||
"dateformat": "^4.0.0",
|
||||
"fs-extra": "^9.0.1"
|
||||
"fs-extra": "^9.0.1",
|
||||
"semver": "^7.3.2",
|
||||
"tar": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ export interface IChannel {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// TODO: Source specification to support plugin services.
|
||||
|
||||
export class ChannelManager {
|
||||
private channels: IChannel[] = [];
|
||||
|
||||
@ -107,4 +109,8 @@ export class ChannelManager {
|
||||
}
|
||||
this.channels.splice(this.channels.indexOf(chan), 1);
|
||||
}
|
||||
|
||||
public getAll(): any[] {
|
||||
return this.channels;
|
||||
}
|
||||
}
|
||||
|
140
src/common/http.ts
Normal file
140
src/common/http.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import qs from 'querystring';
|
||||
import fs from 'fs-extra';
|
||||
import url from 'url';
|
||||
|
||||
export function httpGET(
|
||||
link: string,
|
||||
headers: any = {},
|
||||
restrictToText = true,
|
||||
saveTo?: string,
|
||||
lback?: number): Promise<any> {
|
||||
if (lback && lback >= 4) {
|
||||
throw new Error('infinite loop!');
|
||||
}
|
||||
|
||||
const parsed = url.parse(link);
|
||||
const opts = {
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Accept-Language': 'en-US',
|
||||
'User-Agent': 'Squeebot/Commons-3.0.0',
|
||||
},
|
||||
host: parsed.hostname,
|
||||
path: parsed.path,
|
||||
port: parsed.port,
|
||||
};
|
||||
|
||||
if (headers) {
|
||||
opts.headers = Object.assign(opts.headers, headers);
|
||||
}
|
||||
|
||||
let reqTimeOut: any;
|
||||
let data: string | null = '';
|
||||
|
||||
const httpModule = parsed.protocol === 'https:' ? https : http;
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.get(opts, (res) => {
|
||||
if (res.statusCode === 302 || res.statusCode === 301) {
|
||||
if (!lback) {
|
||||
lback = 1;
|
||||
} else {
|
||||
lback += 1;
|
||||
}
|
||||
|
||||
return httpGET(res.headers.location as string,
|
||||
headers,
|
||||
restrictToText,
|
||||
saveTo,
|
||||
lback,
|
||||
).then(resolve, reject);
|
||||
}
|
||||
|
||||
if (saveTo) {
|
||||
const file = fs.createWriteStream(saveTo);
|
||||
res.pipe(file);
|
||||
file.on('close', () => resolve(saveTo));
|
||||
file.on('error', (e) => reject(e));
|
||||
return;
|
||||
}
|
||||
|
||||
if (restrictToText) {
|
||||
const cType = res.headers['content-type'];
|
||||
if (cType && cType.indexOf('text') === -1 && cType.indexOf('json') === -1) {
|
||||
req.abort();
|
||||
return reject(new Error('Response type is not supported by httpGET!'));
|
||||
}
|
||||
}
|
||||
|
||||
reqTimeOut = setTimeout(() => {
|
||||
req.abort();
|
||||
data = null;
|
||||
reject(new Error('Request took too long!'));
|
||||
}, 5000);
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
clearTimeout(reqTimeOut);
|
||||
|
||||
resolve(data || saveTo);
|
||||
});
|
||||
}).on('error', (e) => {
|
||||
reject(e);
|
||||
});
|
||||
|
||||
req.setTimeout(10000);
|
||||
});
|
||||
}
|
||||
|
||||
export function httpPOST(
|
||||
link: string,
|
||||
headers: any = {},
|
||||
data: any): Promise<any> {
|
||||
const parsed = url.parse(link);
|
||||
let postData = qs.stringify(data);
|
||||
|
||||
const opts = {
|
||||
headers: {
|
||||
'Content-Length': Buffer.byteLength(postData),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'Squeebot/Commons-3.0.0',
|
||||
},
|
||||
host: parsed.host,
|
||||
method: 'POST',
|
||||
path: parsed.path,
|
||||
port: parsed.port,
|
||||
};
|
||||
|
||||
if (headers) {
|
||||
opts.headers = Object.assign(opts.headers, headers);
|
||||
}
|
||||
|
||||
if (opts.headers['Content-Type'] === 'application/json') {
|
||||
postData = JSON.stringify(data);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const httpModule = parsed.protocol === 'https:' ? https : http;
|
||||
const req = httpModule.request(opts, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
let resp = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
resp += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve(resp);
|
||||
});
|
||||
}).on('error', (e) => {
|
||||
reject(e);
|
||||
});
|
||||
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
}
|
3
src/common/index.ts
Normal file
3
src/common/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './http';
|
||||
export * from './time';
|
||||
export * from './sanitize';
|
10
src/common/sanitize.ts
Normal file
10
src/common/sanitize.ts
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
export function sanitizeEscapedText(text: string): string {
|
||||
return text.replace(/\n/g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, '\'')
|
||||
.trim();
|
||||
}
|
116
src/common/time.ts
Normal file
116
src/common/time.ts
Normal file
@ -0,0 +1,116 @@
|
||||
|
||||
export function toHHMMSS(input: string | number): string {
|
||||
const secNum = parseInt(input.toString(), 10);
|
||||
let hours: string | number = Math.floor(secNum / 3600);
|
||||
let minutes: string | number = Math.floor((secNum - (hours * 3600)) / 60);
|
||||
let seconds: string | number = secNum - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {
|
||||
hours = '0' + hours;
|
||||
}
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = '0' + minutes;
|
||||
}
|
||||
|
||||
if (seconds < 10) {
|
||||
seconds = '0' + seconds;
|
||||
}
|
||||
|
||||
let time = '';
|
||||
if (parseInt(hours.toString(), 10) > 0) {
|
||||
time = hours + ':' + minutes + ':' + seconds;
|
||||
} else {
|
||||
time = minutes + ':' + seconds;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
// Add a zero in front of single-digit numbers
|
||||
function zf(v: number): string {
|
||||
return v < 9 ? '0' + v : '' + v;
|
||||
}
|
||||
|
||||
// Convert seconds into years days hours minutes seconds(.milliseconds)
|
||||
export function readableTime(timems: number): string {
|
||||
const time = Math.floor(timems);
|
||||
|
||||
if (time < 60) {
|
||||
return zf(time) + 's';
|
||||
} else if (time < 3600) {
|
||||
return zf(time / 60) +
|
||||
'm ' + zf(time % 60) + 's';
|
||||
} else if (time < 86400) {
|
||||
return zf(time / 3600) +
|
||||
'h ' + zf((time % 3600) / 60) +
|
||||
'm ' + zf((time % 3600) % 60) + 's';
|
||||
} else if (time < 31536000) {
|
||||
return (time / 86400) +
|
||||
'd ' + zf((time % 86400) / 3600) +
|
||||
'h ' + zf((time % 3600) / 60) +
|
||||
'm ' + zf((time % 3600) % 60) + 's';
|
||||
} else {
|
||||
return (time / 31536000) +
|
||||
'y ' + zf((time % 31536000) / 86400) +
|
||||
'd ' + zf((time % 86400) / 3600) +
|
||||
'h ' + zf((time % 3600) / 60) +
|
||||
'm ' + zf((time % 3600) % 60) + 's';
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTimeToSeconds(input: string): number {
|
||||
let seconds = 0;
|
||||
let match;
|
||||
const secMinute = 1 * 60;
|
||||
const secHour = secMinute * 60;
|
||||
const secDay = secHour * 24;
|
||||
const secWeek = secDay * 7;
|
||||
const secYear = secDay * 365;
|
||||
|
||||
match = input.match('([0-9]+)y');
|
||||
if (match != null) {
|
||||
seconds += +match[1] * secYear;
|
||||
}
|
||||
|
||||
match = input.match('([0-9]+)w');
|
||||
if (match != null) {
|
||||
seconds += +match[1] * secWeek;
|
||||
}
|
||||
|
||||
match = input.match('([0-9]+)d');
|
||||
if (match != null) {
|
||||
seconds += +match[1] * secDay;
|
||||
}
|
||||
|
||||
match = input.match('([0-9]+)h');
|
||||
if (match != null) {
|
||||
seconds += +match[1] * secHour;
|
||||
}
|
||||
|
||||
match = input.match('([0-9]+)m');
|
||||
if (match != null) {
|
||||
seconds += +match[1] * secMinute;
|
||||
}
|
||||
|
||||
match = input.match('([0-9]+)s');
|
||||
if (match != null) {
|
||||
seconds += +match[1];
|
||||
}
|
||||
|
||||
return seconds;
|
||||
}
|
||||
|
||||
export function thousandsSeparator(input: number | string): string {
|
||||
const nStr = input.toString();
|
||||
const x = nStr.split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? '.' + x[1] : '';
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + ',' + '$2');
|
||||
}
|
||||
|
||||
return x1 + x2;
|
||||
}
|
@ -25,7 +25,7 @@ export class PluginManager {
|
||||
private configs: PluginConfigurator = new PluginConfigurator(this.environment);
|
||||
|
||||
constructor(
|
||||
private availablePlugins: IPluginManifest[],
|
||||
public availablePlugins: IPluginManifest[],
|
||||
private stream: ScopedEventEmitter,
|
||||
private environment: IEnvironment,
|
||||
private npm: NPMExecutor) {
|
||||
@ -48,6 +48,14 @@ export class PluginManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getLoaded(): IPlugin[] {
|
||||
const list = []
|
||||
for (const pl of this.plugins.values()) {
|
||||
list.push(pl);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public addAvailable(manifest: IPluginManifest | IPluginManifest[]): boolean {
|
||||
// Automatically add arrays of manifests
|
||||
if (Array.isArray(manifest)) {
|
||||
@ -103,16 +111,6 @@ export class PluginManager {
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public removeRepository(repo: string): boolean {
|
||||
const list: IPluginManifest[] = [];
|
||||
for (const mf of this.availablePlugins) {
|
||||
if (mf.repository && mf.repository === repo) {
|
||||
list.push(mf);
|
||||
}
|
||||
}
|
||||
return this.removeAvailable(list);
|
||||
}
|
||||
|
||||
public async load(plugin: IPluginManifest): Promise<IPlugin> {
|
||||
// Don't load plugins twice
|
||||
const ready = this.getLoadedByName(plugin.name);
|
||||
@ -163,6 +161,11 @@ export class PluginManager {
|
||||
// Load the configuration
|
||||
const config: PluginConfiguration = await this.configs.loadConfig(plugin);
|
||||
|
||||
// Ensure plugin has a full path
|
||||
if (!plugin.fullPath) {
|
||||
plugin.fullPath = path.join(this.environment.pluginsPath, plugin.name);
|
||||
}
|
||||
|
||||
// Load the module
|
||||
logger.debug('Loading plugin %s module', plugin.name);
|
||||
const PluginModule = requireNoCache(path.resolve(plugin.fullPath, plugin.main)) as any;
|
||||
@ -243,6 +246,8 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('%s has unloaded.', mf);
|
||||
|
||||
// Delete plugin from the list of loaded plugins
|
||||
this.plugins.delete(mf);
|
||||
|
||||
|
2
src/plugin/repository/index.ts
Normal file
2
src/plugin/repository/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './manager';
|
||||
export * from './repository';
|
239
src/plugin/repository/manager.ts
Normal file
239
src/plugin/repository/manager.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { URL } from 'url';
|
||||
import semver from 'semver';
|
||||
import tar from 'tar';
|
||||
|
||||
import { httpGET } from '../../common';
|
||||
import { logger } from '../../core';
|
||||
|
||||
import { IEnvironment } from "../../types";
|
||||
import { PluginManager } from '../manager';
|
||||
import { IPlugin, IPluginManifest } from "../plugin";
|
||||
import { IRepoPluginDef, IRepository } from "./repository";
|
||||
|
||||
export class RepositoryManager {
|
||||
private repositories: Map<string, IRepository> = new Map();
|
||||
|
||||
constructor(private env: IEnvironment, private plugins: PluginManager) {}
|
||||
|
||||
public repoProvidesPlugin(repo: IRepository, name: string | IPlugin | IPluginManifest): IRepoPluginDef | null {
|
||||
let realName: string;
|
||||
if (typeof name === 'string') {
|
||||
realName = name;
|
||||
} else if ('manifest' in name) {
|
||||
realName = name.manifest.name;
|
||||
} else {
|
||||
realName = name.name;
|
||||
}
|
||||
|
||||
for (const plugin of repo.plugins) {
|
||||
if (plugin.name === realName) {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public findRepoForPlugin(pname: string): IRepository | null {
|
||||
for (const [name, repo] of this.repositories) {
|
||||
for (const plugin of repo.plugins) {
|
||||
if (plugin.name === pname) {
|
||||
return repo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getRepoByName(name: string): IRepository | undefined {
|
||||
return this.repositories.get(name);
|
||||
}
|
||||
|
||||
public getAll(): IRepository[] {
|
||||
const list = []
|
||||
for (const irep of this.repositories.values()) {
|
||||
list.push(irep);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public async installPlugin(name: string): Promise<IPluginManifest> {
|
||||
const repo = this.findRepoForPlugin(name);
|
||||
if (!repo) {
|
||||
throw new Error('Could not find a repository for a plugin named ' + name);
|
||||
}
|
||||
|
||||
const srcFile = name + '.plugin.tgz';
|
||||
const pluginPath = repo.url + '/' + srcFile;
|
||||
const tempdir = path.join(this.env.path, await fs.mkdtemp('.sbdl'));
|
||||
const tempfile = path.join(tempdir, srcFile);
|
||||
let manifest
|
||||
|
||||
try {
|
||||
const save = await httpGET(pluginPath, {}, false, tempfile);
|
||||
const extract = await tar.x({
|
||||
file: tempfile,
|
||||
C: tempdir,
|
||||
});
|
||||
|
||||
const findByName = path.join(tempdir, name);
|
||||
if (!await fs.pathExists(findByName)) {
|
||||
throw new Error('Invalid file provided.');
|
||||
}
|
||||
|
||||
const manifestFile = path.join(findByName, 'plugin.json');
|
||||
if (!await fs.pathExists(manifestFile)) {
|
||||
throw new Error('manifest file does not exist.');
|
||||
}
|
||||
|
||||
const loadManifest = await fs.readJSON(manifestFile);
|
||||
if (!loadManifest.name || !loadManifest.version) {
|
||||
throw new Error('Not a valid plugin manifest file.');
|
||||
}
|
||||
manifest = loadManifest;
|
||||
|
||||
await fs.copy(findByName, path.join(this.env.pluginsPath, manifest.name));
|
||||
} catch (e) {
|
||||
await fs.remove(tempdir);
|
||||
throw e;
|
||||
}
|
||||
|
||||
await fs.remove(tempdir);
|
||||
|
||||
this.plugins.addAvailable(manifest);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public async uninstallPlugin(plugin: string | IPluginManifest | IPlugin): Promise<void> {
|
||||
let realName: string;
|
||||
if (typeof plugin === 'string') {
|
||||
realName = plugin;
|
||||
} else if ('manifest' in plugin) {
|
||||
realName = plugin.manifest.name;
|
||||
} else {
|
||||
realName = plugin.name;
|
||||
}
|
||||
|
||||
const pluginMeta = this.plugins.getAvailableByName(realName);
|
||||
if (!pluginMeta) {
|
||||
throw new Error('No such plugin exists or is available.');
|
||||
}
|
||||
|
||||
const remoPath = path.join(this.env.pluginsPath, realName);
|
||||
await fs.remove(remoPath);
|
||||
|
||||
this.plugins.removeAvailable(pluginMeta);
|
||||
}
|
||||
|
||||
public async getRemote(url: string): Promise<IRepository> {
|
||||
const indexFileGet = await httpGET(url);
|
||||
const meta = JSON.parse(indexFileGet);
|
||||
|
||||
if (!meta.name || !meta.plugins || !meta.schema) {
|
||||
throw new Error('Invalid metadata file for repository.');
|
||||
}
|
||||
|
||||
if (meta.schema > 1) {
|
||||
throw new Error('Unsupported metadata version!');
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
public async installRepository(url: string): Promise<IRepository> {
|
||||
const remote = await this.getRemote(url);
|
||||
const local = path.join(this.env.repositoryPath, remote.name + '.json');
|
||||
const urlParsed = new URL(url);
|
||||
|
||||
// Change the path to not include the metadata file
|
||||
const basePath = path.dirname(urlParsed.pathname);
|
||||
urlParsed.pathname = basePath;
|
||||
remote.url = urlParsed.href;
|
||||
|
||||
await fs.writeJSON(local, remote);
|
||||
|
||||
this.repositories.set(remote.name, remote);
|
||||
return remote;
|
||||
}
|
||||
|
||||
public async uninstallRepository(repo: string | IRepository): Promise<void> {
|
||||
if (typeof repo === 'string') {
|
||||
repo = this.getRepoByName(repo) as IRepository;
|
||||
if (!repo) {
|
||||
throw new Error('No such repository found!');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove repository metadata file
|
||||
const repoFile = path.join(this.env.repositoryPath, repo.name + '.json');
|
||||
await fs.remove(repoFile);
|
||||
this.repositories.delete(repo.name);
|
||||
|
||||
// Uninstall all plugins from this repository
|
||||
for (const plugin of repo.plugins) {
|
||||
// Ignore non-installed plugins
|
||||
try {
|
||||
await this.uninstallPlugin(plugin.name);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
public async checkForUpdates(repo: IRepository): Promise<IPluginManifest[]> {
|
||||
const oprep = await this.updateRepository(repo);
|
||||
|
||||
// Checking for version differences in the plugins
|
||||
// Get locally installed plugins
|
||||
let needsUpdates: IPluginManifest[] = [];
|
||||
for (const avail of this.plugins.availablePlugins) {
|
||||
const repoPlugin = this.repoProvidesPlugin(oprep, avail);
|
||||
if (repoPlugin) {
|
||||
if (semver.gt(repoPlugin.version, avail.version)) {
|
||||
// Plugin needs update
|
||||
needsUpdates.push(avail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return needsUpdates;
|
||||
}
|
||||
|
||||
public async updateRepository(repo: IRepository): Promise<IRepository> {
|
||||
const remote = await this.getRemote(repo.url + '/repository.json');
|
||||
if (remote.created <= repo.created) {
|
||||
return repo;
|
||||
}
|
||||
|
||||
// Repository needs an update
|
||||
logger.debug('Updating repository %s', repo.name);
|
||||
const local = path.join(this.env.repositoryPath, remote.name + '.json');
|
||||
|
||||
// Set the URL to the previous value as it shouldn't change
|
||||
remote.url = repo.url;
|
||||
|
||||
await fs.writeJSON(local, remote);
|
||||
this.repositories.set(remote.name, remote);
|
||||
|
||||
return remote;
|
||||
}
|
||||
|
||||
public async loadFromFiles(): Promise<void> {
|
||||
const repos = await fs.readdir(this.env.repositoryPath);
|
||||
let loaded = [];
|
||||
for (const rf of repos) {
|
||||
const file = path.join(this.env.repositoryPath, rf);
|
||||
try {
|
||||
const contents = await fs.readJSON(file);
|
||||
if (!contents.name || !contents.url || !contents.plugins) {
|
||||
throw new Error('Invalid repository file ' + rf);
|
||||
}
|
||||
loaded.push(contents.name);
|
||||
this.repositories.set(contents.name, contents);
|
||||
} catch (e) {
|
||||
logger.warn('Could not load repository ' + rf);
|
||||
}
|
||||
}
|
||||
logger.debug('Loaded repositories:', loaded.join(', '));
|
||||
}
|
||||
}
|
14
src/plugin/repository/repository.ts
Normal file
14
src/plugin/repository/repository.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { IPluginManifest } from "../plugin";
|
||||
|
||||
export interface IRepoPluginDef {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface IRepository {
|
||||
name: string;
|
||||
created: number;
|
||||
url: string;
|
||||
plugins: IRepoPluginDef[];
|
||||
schema: number;
|
||||
}
|
@ -3,7 +3,7 @@ import * as fs from 'fs-extra';
|
||||
import { IEnvironment } from './environment';
|
||||
|
||||
export class Configuration {
|
||||
private config: any = {};
|
||||
public config: any = {};
|
||||
private loaded = false;
|
||||
|
||||
constructor(private env: IEnvironment, private file: string, private defaults: any = {}) {}
|
||||
@ -62,6 +62,42 @@ export class Configuration {
|
||||
return from[key];
|
||||
}
|
||||
|
||||
public set(key: string, value?: any, from?: any): boolean {
|
||||
if (!from) {
|
||||
from = this.config;
|
||||
}
|
||||
|
||||
// Recursive object traversal
|
||||
if (key.indexOf('.') !== -1) {
|
||||
const split = key.split('.');
|
||||
const first = this.get(split[0], null, from);
|
||||
if (first) {
|
||||
return this.set(split.slice(1).join('.'), value, first);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Array indexing
|
||||
if (key.indexOf('[') !== -1 && key.indexOf(']') !== -1) {
|
||||
const match = key.match(/\[(\d+)\]/i);
|
||||
const realKey = key.substr(0, key.indexOf('['));
|
||||
if (match != null) {
|
||||
const index = parseInt(match[1], 10);
|
||||
if (from[realKey]) {
|
||||
from[realKey][index] = value;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!from[key]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
from[key] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public setDefaults(defconf: any): void {
|
||||
this.defaults = defconf;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { IPlugin } from '../plugin/plugin';
|
||||
import { Protocol } from './protocol';
|
||||
|
||||
// TODO: Source specification to support plugin services.
|
||||
|
||||
export interface IMessage {
|
||||
data: any;
|
||||
source: IPlugin | Protocol;
|
||||
|
Loading…
Reference in New Issue
Block a user