core/src/plugin/repository/manager.ts

240 lines
6.9 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";
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(', '));
}
}