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
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Plugin repository manager
|
|
|
|
*/
|
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) {}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Determine if this repository provides a specific plugin
|
|
|
|
* @param repo Repository manifest
|
|
|
|
* @param mf Plugin manifest
|
|
|
|
* @returns Repository plugin definition
|
|
|
|
*/
|
|
|
|
public repoProvidesPlugin(
|
|
|
|
repo: IRepository,
|
|
|
|
mf: IPluginManifest
|
|
|
|
): IRepoPluginDef | undefined {
|
2021-02-13 12:24:50 +00:00
|
|
|
return repo.plugins.find(plugin => plugin.name === mf.name && repo.name === mf.repository);
|
2020-11-28 19:08:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Find repository providing plugin
|
|
|
|
* @param pname Plugin name
|
|
|
|
* @returns Repository manifest
|
|
|
|
*/
|
2021-02-13 12:24:50 +00:00
|
|
|
public findRepoForPlugin(pname: string): IRepository | undefined {
|
|
|
|
return Array.from(this.repositories.values()).find(repo =>
|
|
|
|
repo.plugins.find(plugin => plugin.name === pname) !== undefined
|
|
|
|
);
|
2020-11-28 19:08:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Find a repository by name
|
|
|
|
* @param name Repository name
|
|
|
|
* @returns Repository manifest
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
public getRepoByName(name: string): IRepository | undefined {
|
|
|
|
return this.repositories.get(name);
|
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Get the list of all installed repositories
|
|
|
|
* @returns All repositories
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
public getAll(): IRepository[] {
|
2021-02-13 12:24:50 +00:00
|
|
|
return Array.from(this.repositories.values());
|
2020-11-28 19:08:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Install a plugin by name
|
|
|
|
* @param name Plugin name
|
|
|
|
* @returns Plugin manifest
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Uninstall a plugin
|
|
|
|
* @param plugin Plugin name, manifest or object
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Get an up-to-date version of a plugin manifest from remote server.
|
|
|
|
* @param url Repository manifest URL
|
|
|
|
* @returns Repository manifest
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Install a repository by remote URL
|
|
|
|
* @param url Remote repository manifest URL
|
|
|
|
* @returns Repository manifest
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
public async installRepository(url: string): Promise<IRepository> {
|
2021-02-13 12:35:03 +00:00
|
|
|
// Add standard repository file path, if missing
|
|
|
|
if (!url.endsWith('/repository.json')) {
|
|
|
|
if (!url.endsWith('/')) {
|
|
|
|
url += '/';
|
|
|
|
}
|
|
|
|
url += 'repository.json';
|
|
|
|
}
|
|
|
|
|
2020-11-28 19:08:23 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Uninstall a repository
|
|
|
|
* @param repo Repository name or manifest
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
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) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Check for plugin updates from this repository.
|
|
|
|
* @param repo Repository manifest
|
|
|
|
* @returns Plugin manifests of plugins that can be updated
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
public async checkForUpdates(repo: IRepository): Promise<IPluginManifest[]> {
|
|
|
|
const oprep = await this.updateRepository(repo);
|
|
|
|
|
|
|
|
// Checking for version differences in the plugins
|
|
|
|
// Get locally installed plugins
|
2021-02-13 12:24:50 +00:00
|
|
|
return this.plugins.availablePlugins.filter(avail => {
|
2020-11-28 19:08:23 +00:00
|
|
|
const repoPlugin = this.repoProvidesPlugin(oprep, avail);
|
2021-02-13 12:24:50 +00:00
|
|
|
if (!repoPlugin) {
|
|
|
|
return false;
|
2020-11-28 19:08:23 +00:00
|
|
|
}
|
2021-02-13 12:24:50 +00:00
|
|
|
return semver.gt(repoPlugin.version, avail.version);
|
|
|
|
});
|
2020-11-28 19:08:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Update a repository manifest by going to the remote.
|
|
|
|
* @param repo Repository manifest
|
|
|
|
* @returns Up-to-date repository manifest
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-10-02 08:07:01 +00:00
|
|
|
/**
|
|
|
|
* Load installed repositories from manifest files.
|
|
|
|
*/
|
2020-11-28 19:08:23 +00:00
|
|
|
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(', '));
|
|
|
|
}
|
|
|
|
}
|