aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/plugins/plugin-manager.ts76
-rw-r--r--server/lib/plugins/yarn.ts61
2 files changed, 134 insertions, 3 deletions
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index b48ecc991..533ed4391 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -1,7 +1,7 @@
1import { PluginModel } from '../../models/server/plugin' 1import { PluginModel } from '../../models/server/plugin'
2import { logger } from '../../helpers/logger' 2import { logger } from '../../helpers/logger'
3import { RegisterHookOptions } from '../../../shared/models/plugins/register.model' 3import { RegisterHookOptions } from '../../../shared/models/plugins/register.model'
4import { join } from 'path' 4import { basename, join } from 'path'
5import { CONFIG } from '../../initializers/config' 5import { CONFIG } from '../../initializers/config'
6import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' 6import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
7import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' 7import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model'
@@ -9,6 +9,7 @@ import { PluginLibrary } from '../../../shared/models/plugins/plugin-library.mod
9import { createReadStream, createWriteStream } from 'fs' 9import { createReadStream, createWriteStream } from 'fs'
10import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' 10import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
11import { PluginType } from '../../../shared/models/plugins/plugin.type' 11import { PluginType } from '../../../shared/models/plugins/plugin.type'
12import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
12 13
13export interface RegisteredPlugin { 14export interface RegisteredPlugin {
14 name: string 15 name: string
@@ -84,11 +85,63 @@ export class PluginManager {
84 await plugin.unregister() 85 await plugin.unregister()
85 } 86 }
86 87
88 async install (toInstall: string, version: string, fromDisk = false) {
89 let plugin: PluginModel
90 let name: string
91
92 logger.info('Installing plugin %s.', toInstall)
93
94 try {
95 fromDisk
96 ? await installNpmPluginFromDisk(toInstall)
97 : await installNpmPlugin(toInstall, version)
98
99 name = fromDisk ? basename(toInstall) : toInstall
100 const pluginType = name.startsWith('peertube-theme-') ? PluginType.THEME : PluginType.PLUGIN
101 const pluginName = this.normalizePluginName(name)
102
103 const packageJSON = this.getPackageJSON(pluginName, pluginType)
104 if (!isPackageJSONValid(packageJSON, pluginType)) {
105 throw new Error('PackageJSON is invalid.')
106 }
107
108 [ plugin ] = await PluginModel.upsert({
109 name: pluginName,
110 description: packageJSON.description,
111 type: pluginType,
112 version: packageJSON.version,
113 enabled: true,
114 uninstalled: false,
115 peertubeEngine: packageJSON.engine.peertube
116 }, { returning: true })
117 } catch (err) {
118 logger.error('Cannot install plugin %s, removing it...', toInstall, { err })
119
120 try {
121 await removeNpmPlugin(name)
122 } catch (err) {
123 logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err })
124 }
125
126 throw err
127 }
128
129 logger.info('Successful installation of plugin %s.', toInstall)
130
131 await this.registerPluginOrTheme(plugin)
132 }
133
134 async uninstall (packageName: string) {
135 await PluginModel.uninstall(this.normalizePluginName(packageName))
136
137 await removeNpmPlugin(packageName)
138 }
139
87 private async registerPluginOrTheme (plugin: PluginModel) { 140 private async registerPluginOrTheme (plugin: PluginModel) {
88 logger.info('Registering plugin or theme %s.', plugin.name) 141 logger.info('Registering plugin or theme %s.', plugin.name)
89 142
90 const pluginPath = join(CONFIG.STORAGE.PLUGINS_DIR, plugin.name, plugin.version) 143 const packageJSON = this.getPackageJSON(plugin.name, plugin.type)
91 const packageJSON: PluginPackageJson = require(join(pluginPath, 'package.json')) 144 const pluginPath = this.getPluginPath(plugin.name, plugin.type)
92 145
93 if (!isPackageJSONValid(packageJSON, plugin.type)) { 146 if (!isPackageJSONValid(packageJSON, plugin.type)) {
94 throw new Error('Package.JSON is invalid.') 147 throw new Error('Package.JSON is invalid.')
@@ -124,6 +177,7 @@ export class PluginManager {
124 } 177 }
125 178
126 const library: PluginLibrary = require(join(pluginPath, packageJSON.library)) 179 const library: PluginLibrary = require(join(pluginPath, packageJSON.library))
180
127 if (!isLibraryCodeValid(library)) { 181 if (!isLibraryCodeValid(library)) {
128 throw new Error('Library code is not valid (miss register or unregister function)') 182 throw new Error('Library code is not valid (miss register or unregister function)')
129 } 183 }
@@ -163,6 +217,22 @@ export class PluginManager {
163 }) 217 })
164 } 218 }
165 219
220 private getPackageJSON (pluginName: string, pluginType: PluginType) {
221 const pluginPath = join(this.getPluginPath(pluginName, pluginType), 'package.json')
222
223 return require(pluginPath) as PluginPackageJson
224 }
225
226 private getPluginPath (pluginName: string, pluginType: PluginType) {
227 const prefix = pluginType === PluginType.PLUGIN ? 'peertube-plugin-' : 'peertube-theme-'
228
229 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', prefix + pluginName)
230 }
231
232 private normalizePluginName (name: string) {
233 return name.replace(/^peertube-((theme)|(plugin))-/, '')
234 }
235
166 static get Instance () { 236 static get Instance () {
167 return this.instance || (this.instance = new this()) 237 return this.instance || (this.instance = new this())
168 } 238 }
diff --git a/server/lib/plugins/yarn.ts b/server/lib/plugins/yarn.ts
new file mode 100644
index 000000000..35fe1625f
--- /dev/null
+++ b/server/lib/plugins/yarn.ts
@@ -0,0 +1,61 @@
1import { execShell } from '../../helpers/core-utils'
2import { logger } from '../../helpers/logger'
3import { isNpmPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
4import { CONFIG } from '../../initializers/config'
5import { outputJSON, pathExists } from 'fs-extra'
6import { join } from 'path'
7
8async function installNpmPlugin (name: string, version: string) {
9 // Security check
10 checkNpmPluginNameOrThrow(name)
11 checkPluginVersionOrThrow(version)
12
13 const toInstall = `${name}@${version}`
14 await execYarn('add ' + toInstall)
15}
16
17async function installNpmPluginFromDisk (path: string) {
18 await execYarn('add file:' + path)
19}
20
21async function removeNpmPlugin (name: string) {
22 checkNpmPluginNameOrThrow(name)
23
24 await execYarn('remove ' + name)
25}
26
27// ############################################################################
28
29export {
30 installNpmPlugin,
31 installNpmPluginFromDisk,
32 removeNpmPlugin
33}
34
35// ############################################################################
36
37async function execYarn (command: string) {
38 try {
39 const pluginDirectory = CONFIG.STORAGE.PLUGINS_DIR
40 const pluginPackageJSON = join(pluginDirectory, 'package.json')
41
42 // Create empty package.json file if needed
43 if (!await pathExists(pluginPackageJSON)) {
44 await outputJSON(pluginPackageJSON, {})
45 }
46
47 await execShell(`yarn ${command}`, { cwd: pluginDirectory })
48 } catch (result) {
49 logger.error('Cannot exec yarn.', { command, err: result.err, stderr: result.stderr })
50
51 throw result.err
52 }
53}
54
55function checkNpmPluginNameOrThrow (name: string) {
56 if (!isNpmPluginNameValid(name)) throw new Error('Invalid NPM plugin name to install')
57}
58
59function checkPluginVersionOrThrow (name: string) {
60 if (!isPluginVersionValid(name)) throw new Error('Invalid NPM plugin version to install')
61}