diff options
Diffstat (limited to 'server/lib/plugins/plugin-manager.ts')
-rw-r--r-- | server/lib/plugins/plugin-manager.ts | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts new file mode 100644 index 000000000..b48ecc991 --- /dev/null +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -0,0 +1,169 @@ | |||
1 | import { PluginModel } from '../../models/server/plugin' | ||
2 | import { logger } from '../../helpers/logger' | ||
3 | import { RegisterHookOptions } from '../../../shared/models/plugins/register.model' | ||
4 | import { join } from 'path' | ||
5 | import { CONFIG } from '../../initializers/config' | ||
6 | import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' | ||
7 | import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' | ||
8 | import { PluginLibrary } from '../../../shared/models/plugins/plugin-library.model' | ||
9 | import { createReadStream, createWriteStream } from 'fs' | ||
10 | import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' | ||
11 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | ||
12 | |||
13 | export interface RegisteredPlugin { | ||
14 | name: string | ||
15 | version: string | ||
16 | description: string | ||
17 | peertubeEngine: string | ||
18 | |||
19 | type: PluginType | ||
20 | |||
21 | path: string | ||
22 | |||
23 | staticDirs: { [name: string]: string } | ||
24 | |||
25 | css: string[] | ||
26 | |||
27 | // Only if this is a plugin | ||
28 | unregister?: Function | ||
29 | } | ||
30 | |||
31 | export interface HookInformationValue { | ||
32 | pluginName: string | ||
33 | handler: Function | ||
34 | priority: number | ||
35 | } | ||
36 | |||
37 | export class PluginManager { | ||
38 | |||
39 | private static instance: PluginManager | ||
40 | |||
41 | private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} | ||
42 | private hooks: { [ name: string ]: HookInformationValue[] } = {} | ||
43 | |||
44 | private constructor () { | ||
45 | } | ||
46 | |||
47 | async registerPlugins () { | ||
48 | const plugins = await PluginModel.listEnabledPluginsAndThemes() | ||
49 | |||
50 | for (const plugin of plugins) { | ||
51 | try { | ||
52 | await this.registerPluginOrTheme(plugin) | ||
53 | } catch (err) { | ||
54 | logger.error('Cannot register plugin %s, skipping.', plugin.name, { err }) | ||
55 | } | ||
56 | } | ||
57 | |||
58 | this.sortHooksByPriority() | ||
59 | } | ||
60 | |||
61 | getRegisteredPlugin (name: string) { | ||
62 | return this.registeredPlugins[ name ] | ||
63 | } | ||
64 | |||
65 | getRegisteredTheme (name: string) { | ||
66 | const registered = this.getRegisteredPlugin(name) | ||
67 | |||
68 | if (!registered || registered.type !== PluginType.THEME) return undefined | ||
69 | |||
70 | return registered | ||
71 | } | ||
72 | |||
73 | async unregister (name: string) { | ||
74 | const plugin = this.getRegisteredPlugin(name) | ||
75 | |||
76 | if (!plugin) { | ||
77 | throw new Error(`Unknown plugin ${name} to unregister`) | ||
78 | } | ||
79 | |||
80 | if (plugin.type === PluginType.THEME) { | ||
81 | throw new Error(`Cannot unregister ${name}: this is a theme`) | ||
82 | } | ||
83 | |||
84 | await plugin.unregister() | ||
85 | } | ||
86 | |||
87 | private async registerPluginOrTheme (plugin: PluginModel) { | ||
88 | logger.info('Registering plugin or theme %s.', plugin.name) | ||
89 | |||
90 | const pluginPath = join(CONFIG.STORAGE.PLUGINS_DIR, plugin.name, plugin.version) | ||
91 | const packageJSON: PluginPackageJson = require(join(pluginPath, 'package.json')) | ||
92 | |||
93 | if (!isPackageJSONValid(packageJSON, plugin.type)) { | ||
94 | throw new Error('Package.JSON is invalid.') | ||
95 | } | ||
96 | |||
97 | let library: PluginLibrary | ||
98 | if (plugin.type === PluginType.PLUGIN) { | ||
99 | library = await this.registerPlugin(plugin, pluginPath, packageJSON) | ||
100 | } | ||
101 | |||
102 | this.registeredPlugins[ plugin.name ] = { | ||
103 | name: plugin.name, | ||
104 | type: plugin.type, | ||
105 | version: plugin.version, | ||
106 | description: plugin.description, | ||
107 | peertubeEngine: plugin.peertubeEngine, | ||
108 | path: pluginPath, | ||
109 | staticDirs: packageJSON.staticDirs, | ||
110 | css: packageJSON.css, | ||
111 | unregister: library ? library.unregister : undefined | ||
112 | } | ||
113 | } | ||
114 | |||
115 | private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) { | ||
116 | const registerHook = (options: RegisterHookOptions) => { | ||
117 | if (!this.hooks[options.target]) this.hooks[options.target] = [] | ||
118 | |||
119 | this.hooks[options.target].push({ | ||
120 | pluginName: plugin.name, | ||
121 | handler: options.handler, | ||
122 | priority: options.priority || 0 | ||
123 | }) | ||
124 | } | ||
125 | |||
126 | const library: PluginLibrary = require(join(pluginPath, packageJSON.library)) | ||
127 | if (!isLibraryCodeValid(library)) { | ||
128 | throw new Error('Library code is not valid (miss register or unregister function)') | ||
129 | } | ||
130 | |||
131 | library.register({ registerHook }) | ||
132 | |||
133 | logger.info('Add plugin %s CSS to global file.', plugin.name) | ||
134 | |||
135 | await this.addCSSToGlobalFile(pluginPath, packageJSON.css) | ||
136 | |||
137 | return library | ||
138 | } | ||
139 | |||
140 | private sortHooksByPriority () { | ||
141 | for (const hookName of Object.keys(this.hooks)) { | ||
142 | this.hooks[hookName].sort((a, b) => { | ||
143 | return b.priority - a.priority | ||
144 | }) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | private async addCSSToGlobalFile (pluginPath: string, cssRelativePaths: string[]) { | ||
149 | for (const cssPath of cssRelativePaths) { | ||
150 | await this.concatFiles(join(pluginPath, cssPath), PLUGIN_GLOBAL_CSS_PATH) | ||
151 | } | ||
152 | } | ||
153 | |||
154 | private concatFiles (input: string, output: string) { | ||
155 | return new Promise<void>((res, rej) => { | ||
156 | const outputStream = createWriteStream(input) | ||
157 | const inputStream = createReadStream(output) | ||
158 | |||
159 | inputStream.pipe(outputStream) | ||
160 | |||
161 | inputStream.on('end', () => res()) | ||
162 | inputStream.on('error', err => rej(err)) | ||
163 | }) | ||
164 | } | ||
165 | |||
166 | static get Instance () { | ||
167 | return this.instance || (this.instance = new this()) | ||
168 | } | ||
169 | } | ||