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 = 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 { 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 { 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 { 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 { 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 { 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 { const oprep = await this.updateRepository(repo); // Checking for version differences in the plugins // Get locally installed plugins const 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 { 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 { 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); } 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(', ')); } }