core/src/plugin/repository/manager.ts

302 lines
8.6 KiB
TypeScript

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';
/**
* Plugin repository manager
*/
export class RepositoryManager {
private repositories: Map<string, IRepository> = new Map();
constructor(private env: IEnvironment, private plugins: PluginManager) {}
/**
* 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 {
return repo.plugins.find(plugin => plugin.name === mf.name && repo.name === mf.repository);
}
/**
* Find repository providing plugin
* @param pname Plugin name
* @returns Repository manifest
*/
public findRepoForPlugin(pname: string): IRepository | undefined {
return Array.from(this.repositories.values()).find(repo =>
repo.plugins.find(plugin => plugin.name === pname) !== undefined
);
}
/**
* Find a repository by name
* @param name Repository name
* @returns Repository manifest
*/
public getRepoByName(name: string): IRepository | undefined {
return this.repositories.get(name);
}
/**
* Get the list of all installed repositories
* @returns All repositories
*/
public getAll(): IRepository[] {
return Array.from(this.repositories.values());
}
/**
* Install a plugin by name
* @param name Plugin name
* @returns Plugin manifest
*/
public async installPlugin(name: string): Promise<IPluginManifest> {
let repo;
if (name.indexOf('/') !== -1) {
const naming = name.split('/');
repo = this.getRepoByName(naming[0]);
name = naming[1];
} else {
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 = await fs.mkdtemp(path.join(this.env.path, '.sbdl'));
const tempfile = path.join(tempdir, srcFile);
let manifest: IPluginManifest;
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;
const fp = path.join(this.env.pluginsPath, manifest.name);
manifest.fullPath = fp;
manifest.repository = repo.name;
await fs.writeJSON(manifestFile, manifest);
await fs.copy(findByName, fp);
} catch (e) {
await fs.remove(tempdir);
throw e;
}
await fs.remove(tempdir);
this.plugins.addAvailable(manifest);
return manifest;
}
/**
* Uninstall a plugin
* @param plugin Plugin name, manifest or object
*/
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);
}
/**
* Get an up-to-date version of a plugin manifest from remote server.
* @param url Repository manifest URL
* @returns Repository manifest
*/
public async getRemote(url: string): Promise<IRepository> {
const indexFileGet = await httpGET(url);
const meta = JSON.parse(indexFileGet);
if (!meta.name || !meta.plugins || !(meta.$schema || meta.schema)) {
throw new Error('Invalid metadata file for repository.');
}
if (!(/^[a-zA-Z0-9_\-+]+$/.test(meta.name))) {
throw new Error('Illegal name for repository!');
}
if (meta.schema > 1) {
throw new Error('Unsupported metadata version!');
}
return meta;
}
/**
* Install a repository by remote URL
* @param url Remote repository manifest URL
* @returns Repository manifest
*/
public async installRepository(url: string): Promise<IRepository> {
// Add standard repository file path, if missing
if (!url.endsWith('/repository.json')) {
if (!url.endsWith('/')) {
url += '/';
}
url += 'repository.json';
}
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;
}
/**
* Uninstall a repository
* @param repo Repository name or manifest
*/
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) {}
}
}
/**
* Check for plugin updates from this repository.
* @param repo Repository manifest
* @returns Plugin manifests of plugins that can be updated
*/
public async checkForUpdates(repo: IRepository): Promise<IPluginManifest[]> {
const oprep = await this.updateRepository(repo);
// Checking for version differences in the plugins
// Get locally installed plugins
return this.plugins.availablePlugins.filter(avail => {
const repoPlugin = this.repoProvidesPlugin(oprep, avail);
if (!repoPlugin) {
return false;
}
return semver.gt(repoPlugin.version, avail.version);
});
}
/**
* Update a repository manifest by going to the remote.
* @param repo Repository manifest
* @returns Up-to-date repository manifest
*/
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;
}
/**
* Load installed repositories from manifest files.
*/
public async loadFromFiles(): Promise<void> {
const repos = await fs.readdir(this.env.repositoryPath);
const 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);
}
if (!(/^[a-zA-Z0-9_\-+]+$/.test(contents.name))) {
throw new Error(`"${rf}" is an illegal name for a repository!`);
}
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(', '));
}
}