2020-11-28 19:08:23 +00:00
|
|
|
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';
|
|
|
|
|
2020-11-28 20:27:05 +00:00
|
|
|
import { IEnvironment } from '../../types';
|
2020-11-28 19:08:23 +00:00
|
|
|
import { PluginManager } from '../manager';
|
2020-11-28 20:27:05 +00:00
|
|
|
import { IPlugin, IPluginManifest } from '../plugin';
|
|
|
|
import { IRepoPluginDef, IRepository } from './repository';
|
2020-11-28 19:08:23 +00:00
|
|
|
|
|
|
|
export class RepositoryManager {
|
|
|
|
private repositories: Map<string, IRepository> = new Map();
|
|
|
|
|
|
|
|
constructor(private env: IEnvironment, private plugins: PluginManager) {}
|
|
|
|
|
2020-12-03 17:45:25 +00:00
|
|
|
public repoProvidesPlugin(repo: IRepository, mf: IPluginManifest): IRepoPluginDef | null {
|
2020-11-28 19:08:23 +00:00
|
|
|
for (const plugin of repo.plugins) {
|
2020-12-03 17:45:25 +00:00
|
|
|
if (plugin.name === mf.name && mf.repository === repo.name) {
|
2020-11-28 19:08:23 +00:00
|
|
|
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[] {
|
2020-11-28 20:27:05 +00:00
|
|
|
const list = [];
|
2020-11-28 19:08:23 +00:00
|
|
|
for (const irep of this.repositories.values()) {
|
|
|
|
list.push(irep);
|
|
|
|
}
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async installPlugin(name: string): Promise<IPluginManifest> {
|
2020-12-03 17:45:25 +00:00
|
|
|
let repo;
|
|
|
|
if (name.indexOf('/') !== -1) {
|
|
|
|
const naming = name.split('/');
|
|
|
|
repo = this.getRepoByName(naming[0]);
|
|
|
|
name = naming[1];
|
|
|
|
} else {
|
|
|
|
repo = this.findRepoForPlugin(name);
|
|
|
|
}
|
|
|
|
|
2020-11-28 19:08:23 +00:00
|
|
|
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;
|
2020-12-03 17:45:25 +00:00
|
|
|
const tempdir = await fs.mkdtemp(path.join(this.env.path, '.sbdl'));
|
2020-11-28 19:08:23 +00:00
|
|
|
const tempfile = path.join(tempdir, srcFile);
|
|
|
|
|
2020-12-03 17:45:25 +00:00
|
|
|
let manifest: IPluginManifest;
|
2020-11-28 19:08:23 +00:00
|
|
|
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;
|
2020-12-03 17:45:25 +00:00
|
|
|
const fp = path.join(this.env.pluginsPath, manifest.name);
|
|
|
|
manifest.fullPath = fp;
|
|
|
|
manifest.repository = repo.name;
|
2020-11-28 19:08:23 +00:00
|
|
|
|
2020-12-03 17:45:25 +00:00
|
|
|
await fs.writeJSON(manifestFile, manifest);
|
|
|
|
await fs.copy(findByName, fp);
|
2020-11-28 19:08:23 +00:00
|
|
|
} 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.');
|
|
|
|
}
|
|
|
|
|
2020-11-29 11:35:45 +00:00
|
|
|
if (!(/^[a-zA-Z0-9_\-\+]+$/.test(meta.name))) {
|
|
|
|
throw new Error('Illegal name for repository!');
|
|
|
|
}
|
|
|
|
|
2020-11-28 19:08:23 +00:00
|
|
|
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);
|
2020-11-28 20:27:05 +00:00
|
|
|
|
2020-11-28 19:08:23 +00:00
|
|
|
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
|
2020-11-28 20:27:05 +00:00
|
|
|
const needsUpdates: IPluginManifest[] = [];
|
2020-11-28 19:08:23 +00:00
|
|
|
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);
|
2020-11-28 20:27:05 +00:00
|
|
|
const loaded = [];
|
2020-11-28 19:08:23 +00:00
|
|
|
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);
|
|
|
|
}
|
2020-11-29 11:35:45 +00:00
|
|
|
|
|
|
|
if (!(/^[a-zA-Z0-9_\-\+]+$/.test(contents.name))) {
|
|
|
|
throw new Error(`"${rf}" is an illegal name for a repository!`);
|
|
|
|
}
|
|
|
|
|
2020-11-28 19:08:23 +00:00
|
|
|
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(', '));
|
|
|
|
}
|
|
|
|
}
|