From 97b06f2455c0fbbce647fae6e8fe226b71fe82a4 Mon Sep 17 00:00:00 2001 From: Infinidoge Date: Fri, 16 Aug 2024 03:04:47 -0400 Subject: [PATCH] Allow installing plugins declaratively --- server/core/lib/plugins/plugin-manager.ts | 88 ++++++++++++++++++++++- server/core/lib/plugins/yarn.ts | 6 +- server/core/models/server/plugin.ts | 15 ++++ server/server.ts | 1 + 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/server/core/lib/plugins/plugin-manager.ts b/server/core/lib/plugins/plugin-manager.ts index 66b5c5b1841..2919b7e3efa 100644 --- a/server/core/lib/plugins/plugin-manager.ts +++ b/server/core/lib/plugins/plugin-manager.ts @@ -20,6 +20,7 @@ import { decachePlugin } from '@server/helpers/decache.js' import { ApplicationModel } from '@server/models/application/application.js' import { MOAuthTokenUser, MUser } from '@server/types/models/index.js' import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins.js' +import { execShell } from '../../helpers/core-utils.js' import { logger } from '../../helpers/logger.js' import { CONFIG } from '../../initializers/config.js' import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants.js' @@ -334,13 +335,93 @@ export class PluginManager implements ServerHook { // ###################### Installation ###################### + async installDeclarativePlugins () { + const pluginDirectory = CONFIG.STORAGE.PLUGINS_DIR + let declaredPlugins + try { + declaredPlugins = await readJSON(join(pluginDirectory, "declarative_plugins.json")); + } + catch (err) { + logger.info("No declarative plugins file found.") + return + } + + const existingDeclaredPlugins: PluginModel[] = await PluginModel.listDeclarativePluginsAndThemes() + + for (const plugin of existingDeclaredPlugins) { + const npmName = PluginModel.buildNpmName(plugin.name, plugin.type) + if (!(npmName in declaredPlugins)) { + logger.info("Removing previously declared plugin %s", npmName) + await this.uninstall({npmName: npmName, unregister: false}) + } + else { + logger.info("Keeping still-declared plugin %s.", npmName) + } + } + + for (const npmName of Object.keys(declaredPlugins)) { + const { + pluginPath = null, + version = null, + extraArgs = null, + preInstall = null, + postInstall = null + } = declaredPlugins[npmName]; + + if (preInstall != null) { + logger.info("Running pre-install command for plugin %s.", npmName) + try { + await execShell(preInstall, { cwd: pluginDirectory }) + } catch (result) { + logger.error("Cannot exec pre-install command.", { preInstall, err: result.err, stderr: result.stderr }) + + throw result.err + } + } + + if (pluginPath != null) { + logger.info("Installing declared plugin %s from disk (path %s).", npmName, pluginPath) + await this.install({ + toInstall: pluginPath, + fromDisk: true, + declarative: true, + extraArgs: extraArgs, + }) + } + else if (version != null) { + logger.info("Installing declared plugin %s (version %s) from npm.", npmName, version) + await this.install({ + toInstall: npmName, + version: version, + declarative: true, + extraArgs: extraArgs, + }) + } + + if (postInstall != null) { + logger.info("Running post-install command for plugin %s.", npmName) + try { + await execShell(postInstall, { cwd: pluginDirectory }) + } catch (result) { + logger.error("Cannot exec post-install command.", { postInstall, err: result.err, stderr: result.stderr }) + + throw result.err + } + } + } + + this.sortHooksByPriority() + } + async install (options: { toInstall: string version?: string fromDisk?: boolean // default false register?: boolean // default true + declarative?: boolean // default false + extraArgs?: string // default empty }) { - const { toInstall, version, fromDisk = false, register = true } = options + const { toInstall, version, fromDisk = false, register = true, declarative = false, extraArgs = null } = options let plugin: PluginModel let npmName: string @@ -349,8 +430,8 @@ export class PluginManager implements ServerHook { try { fromDisk - ? await installNpmPluginFromDisk(toInstall) - : await installNpmPlugin(toInstall, version) + ? await installNpmPluginFromDisk(toInstall, extraArgs) + : await installNpmPlugin(toInstall, version, extraArgs) npmName = fromDisk ? basename(toInstall) : toInstall const pluginType = PluginModel.getTypeFromNpmName(npmName) @@ -368,6 +449,7 @@ export class PluginManager implements ServerHook { version: packageJSON.version, enabled: true, uninstalled: false, + declarative: declarative, peertubeEngine: packageJSON.engine.peertube }, { returning: true }) diff --git a/server/core/lib/plugins/yarn.ts b/server/core/lib/plugins/yarn.ts index 470880f4439..6943b8716e7 100644 --- a/server/core/lib/plugins/yarn.ts +++ b/server/core/lib/plugins/yarn.ts @@ -6,7 +6,7 @@ import { logger } from '../../helpers/logger.js' import { CONFIG } from '../../initializers/config.js' import { getLatestPluginVersion } from './plugin-index.js' -async function installNpmPlugin (npmName: string, versionArg?: string) { +async function installNpmPlugin (npmName: string, versionArg?: string, extraArgs?: string) { // Security check checkNpmPluginNameOrThrow(npmName) if (versionArg) checkPluginVersionOrThrow(versionArg) @@ -15,13 +15,15 @@ async function installNpmPlugin (npmName: string, versionArg?: string) { let toInstall = npmName if (version) toInstall += `@${version}` + if (extraArgs) toInstall += ` ${extraArgs}` const { stdout } = await execYarn('add ' + toInstall) logger.debug('Added a yarn package.', { yarnStdout: stdout }) } -async function installNpmPluginFromDisk (path: string) { +async function installNpmPluginFromDisk (path: string, extraArgs?: string) { + if (extraArgs) path += ` ${extraArgs}` await execYarn('add file:' + path) } diff --git a/server/core/models/server/plugin.ts b/server/core/models/server/plugin.ts index daf31b80816..3753c339939 100644 --- a/server/core/models/server/plugin.ts +++ b/server/core/models/server/plugin.ts @@ -86,6 +86,10 @@ export class PluginModel extends SequelizeModel { @Column(DataType.JSONB) storage: any + @AllowNull(true) + @Column + declarative: boolean + @CreatedAt createdAt: Date @@ -103,6 +107,17 @@ export class PluginModel extends SequelizeModel { return PluginModel.findAll(query) } + static listDeclarativePluginsAndThemes (): Promise { + const query = { + where: { + declarative: true, + uninstalled: false + } + } + + return PluginModel.findAll(query) + } + static loadByNpmName (npmName: string): Promise { const name = this.normalizePluginName(npmName) const type = this.getTypeFromNpmName(npmName) diff --git a/server/server.ts b/server/server.ts index 23e6cb1e613..5c75befecd3 100644 --- a/server/server.ts +++ b/server/server.ts @@ -352,6 +352,7 @@ async function startApplication () { server.listen(port, hostname, async () => { if (cliOptions.plugins) { try { + await PluginManager.Instance.installDeclarativePlugins() await PluginManager.Instance.rebuildNativePluginsIfNeeded() await PluginManager.Instance.registerPluginsAndThemes()