diff options
author | Chocobozzz <me@florianbigard.com> | 2019-07-05 15:28:49 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-07-24 10:58:16 +0200 |
commit | f023a19c3eeeea2b014b47fae522a62eab320048 (patch) | |
tree | 988ff97432663db928f1e3e3f498da856e739de1 /server/lib/plugins | |
parent | 345da516fae80f24c90c2196e96393b489af2243 (diff) | |
download | PeerTube-f023a19c3eeeea2b014b47fae522a62eab320048.tar.gz PeerTube-f023a19c3eeeea2b014b47fae522a62eab320048.tar.zst PeerTube-f023a19c3eeeea2b014b47fae522a62eab320048.zip |
WIP plugins: install/uninstall
Diffstat (limited to 'server/lib/plugins')
-rw-r--r-- | server/lib/plugins/plugin-manager.ts | 76 | ||||
-rw-r--r-- | server/lib/plugins/yarn.ts | 61 |
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 @@ | |||
1 | import { PluginModel } from '../../models/server/plugin' | 1 | import { PluginModel } from '../../models/server/plugin' |
2 | import { logger } from '../../helpers/logger' | 2 | import { logger } from '../../helpers/logger' |
3 | import { RegisterHookOptions } from '../../../shared/models/plugins/register.model' | 3 | import { RegisterHookOptions } from '../../../shared/models/plugins/register.model' |
4 | import { join } from 'path' | 4 | import { basename, join } from 'path' |
5 | import { CONFIG } from '../../initializers/config' | 5 | import { CONFIG } from '../../initializers/config' |
6 | import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' | 6 | import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' |
7 | import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' | 7 | import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' |
@@ -9,6 +9,7 @@ import { PluginLibrary } from '../../../shared/models/plugins/plugin-library.mod | |||
9 | import { createReadStream, createWriteStream } from 'fs' | 9 | import { createReadStream, createWriteStream } from 'fs' |
10 | import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' | 10 | import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' |
11 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 11 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
12 | import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' | ||
12 | 13 | ||
13 | export interface RegisteredPlugin { | 14 | export 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 @@ | |||
1 | import { execShell } from '../../helpers/core-utils' | ||
2 | import { logger } from '../../helpers/logger' | ||
3 | import { isNpmPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' | ||
4 | import { CONFIG } from '../../initializers/config' | ||
5 | import { outputJSON, pathExists } from 'fs-extra' | ||
6 | import { join } from 'path' | ||
7 | |||
8 | async 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 | |||
17 | async function installNpmPluginFromDisk (path: string) { | ||
18 | await execYarn('add file:' + path) | ||
19 | } | ||
20 | |||
21 | async function removeNpmPlugin (name: string) { | ||
22 | checkNpmPluginNameOrThrow(name) | ||
23 | |||
24 | await execYarn('remove ' + name) | ||
25 | } | ||
26 | |||
27 | // ############################################################################ | ||
28 | |||
29 | export { | ||
30 | installNpmPlugin, | ||
31 | installNpmPluginFromDisk, | ||
32 | removeNpmPlugin | ||
33 | } | ||
34 | |||
35 | // ############################################################################ | ||
36 | |||
37 | async 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 | |||
55 | function checkNpmPluginNameOrThrow (name: string) { | ||
56 | if (!isNpmPluginNameValid(name)) throw new Error('Invalid NPM plugin name to install') | ||
57 | } | ||
58 | |||
59 | function checkPluginVersionOrThrow (name: string) { | ||
60 | if (!isPluginVersionValid(name)) throw new Error('Invalid NPM plugin version to install') | ||
61 | } | ||