-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathinstaller.ts
More file actions
219 lines (180 loc) · 7.34 KB
/
installer.ts
File metadata and controls
219 lines (180 loc) · 7.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
'use strict';
import log4js from "log4js";
import axios, {AxiosResponse} from "axios";
import {PackageData, PackageInfo} from "../../../node/types/PackageInfo";
import {MapArrayType} from "../../../node/types/MapType";
import path from "path";
import {promises as fs} from "fs";
const plugins = require('./plugins');
const hooks = require('./hooks');
const runCmd = require('../../../node/utils/run_cmd');
import settings, {
getEpVersion,
reloadSettings
} from '../../../node/utils/Settings';
import {LinkInstaller} from "./LinkInstaller";
import {findEtherpadRoot} from '../../../node/utils/AbsolutePaths';
const logger = log4js.getLogger('plugins');
export const pluginInstallPath = path.join(settings.root, 'src','plugin_packages');
export const node_modules = path.join(findEtherpadRoot(),'src', 'node_modules');
export const installedPluginsPath = path.join(settings.root, 'var/installed_plugins.json');
const onAllTasksFinished = async () => {
await plugins.update();
await persistInstalledPlugins();
reloadSettings();
await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('restartServer');
};
const headers = {
'User-Agent': `Etherpad/${getEpVersion()}`,
};
let tasks = 0;
export const linkInstaller = new LinkInstaller();
const wrapTaskCb = (cb:Function|null) => {
tasks++;
return (...args: any) => {
cb && cb(...args);
tasks--;
if (tasks === 0) onAllTasksFinished();
};
};
const migratePluginsFromNodeModules = async () => {
logger.info('start migration of plugins in node_modules');
// Notes:
// * Do not pass `--prod` otherwise `npm ls` will fail if there is no `package.json`.
// * The `--no-production` flag is required (or the `NODE_ENV` environment variable must be
// unset or set to `development`) because otherwise `npm ls` will not mention any packages
// that are not included in `package.json` (which is expected to not exist).
const cmd = ['pnpm', 'ls', '--long', '--json', '--depth=0', '--no-production'];
const [{dependencies = {}}] = JSON.parse(await runCmd(cmd,
{stdio: [null, 'string']}));
await Promise.all(Object.entries(dependencies)
.filter(([pkg, info]) => pkg.startsWith(plugins.prefix) && pkg !== 'ep_etherpad-lite')
.map(async ([pkg, info]) => {
const _info = info as PackageInfo
if (!_info.resolved) {
// Install from node_modules directory
await linkInstaller.installFromPath(`${findEtherpadRoot()}/node_modules/${pkg}`);
} else {
await linkInstaller.installPlugin(pkg);
}
}));
await persistInstalledPlugins();
};
export const checkForMigration = async () => {
logger.info('check installed plugins for migration');
// Initialize linkInstaller
await linkInstaller.init()
try {
await fs.access(installedPluginsPath, fs.constants.F_OK);
} catch (err) {
await migratePluginsFromNodeModules();
}
/*
* Check if the plugin is already installed in node_modules
* If not, create a symlink to node_modules
* This is necessary as
* 1. Live Plugin Manager does not support loading plugins from the directory so that node can access them normally
* 2. Plugins can't be directly installed to node_modules otherwise upgrading Etherpad will remove them
*/
fs.stat(pluginInstallPath).then(async (err) => {
const files = await fs.readdir(pluginInstallPath);
for (let file of files){
const moduleName = path.basename(file);
if (moduleName === '.versions') {
// Skip the directory using live-plugin-manager
continue;
}
try {
await fs.access(path.join(node_modules, moduleName), fs.constants.F_OK);
logger.debug(`plugin ${moduleName} already exists in node_modules`);
} catch (err) {
// Create symlink to node_modules
logger.debug(`create symlink for ${file} to ${path.join(node_modules,moduleName)}`)
await fs.symlink(path.join(pluginInstallPath,file), path.join(node_modules,moduleName), 'dir')
}
}
}).catch(()=>{
logger.debug('plugin directory does not exist');
})
const fileContent = await fs.readFile(installedPluginsPath);
const installedPlugins = JSON.parse(fileContent.toString());
for (const plugin of installedPlugins.plugins) {
if (plugin.name.startsWith(plugins.prefix) && plugin.name !== 'ep_etherpad-lite') {
try {
await linkInstaller.installPlugin(plugin.name, plugin.version);
} catch (e) {
logger.error(`Error installing plugin ${plugin.name} with version ${plugin.version}: ${e}`);
}
}
}
};
const persistInstalledPlugins = async () => {
const installedPlugins:{
plugins: PackageData[]
} = {plugins: []};
for (const pkg of Object.values(await plugins.getPackages()) as PackageData[]) {
installedPlugins.plugins.push({
name: pkg.name,
version: pkg.version,
});
}
installedPlugins.plugins = [...new Set(installedPlugins.plugins)];
await fs.writeFile(installedPluginsPath, JSON.stringify(installedPlugins));
};
export const uninstall = async (pluginName: string, cb:Function|null = null) => {
cb = wrapTaskCb(cb);
logger.info(`Uninstalling plugin ${pluginName}...`);
await linkInstaller.uninstallPlugin(pluginName);
logger.info(`Successfully uninstalled plugin ${pluginName}`);
await hooks.aCallAll('pluginUninstall', {pluginName});
cb(null);
};
export const install = async (pluginName: string, cb:Function|null = null) => {
cb = wrapTaskCb(cb);
logger.info(`Installing plugin ${pluginName}...`);
await linkInstaller.installPlugin(pluginName);
logger.info(`Successfully installed plugin ${pluginName}`);
await hooks.aCallAll('pluginInstall', {pluginName});
cb(null);
};
export let availablePlugins:MapArrayType<PackageInfo>|null = null;
let cacheTimestamp = 0;
export const getAvailablePlugins = async (maxCacheAge: number | false) => {
const nowTimestamp = Math.round(Date.now() / 1000);
// check cache age before making any request
if (availablePlugins && maxCacheAge && (nowTimestamp - cacheTimestamp) <= maxCacheAge) {
return availablePlugins;
}
const pluginsLoaded: AxiosResponse<MapArrayType<PackageInfo>> = await axios.get(`${settings.updateServer}/plugins.json`, {headers})
availablePlugins = pluginsLoaded.data;
cacheTimestamp = nowTimestamp;
return availablePlugins;
};
export const search = (searchTerm: string, maxCacheAge: number) => getAvailablePlugins(maxCacheAge).then(
(results: MapArrayType<PackageInfo>) => {
const res:MapArrayType<PackageData> = {};
if (searchTerm) {
searchTerm = searchTerm.toLowerCase();
}
for (const pluginName in results) {
// for every available plugin
// TODO: Also search in keywords here!
if (pluginName.indexOf(plugins.prefix) !== 0) continue;
if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) &&
(typeof results[pluginName].description !== 'undefined' &&
!~results[pluginName].description.toLowerCase().indexOf(searchTerm))
) {
if (typeof results[pluginName].description === 'undefined') {
logger.debug(`plugin without Description: ${results[pluginName].name}`);
}
continue;
}
res[pluginName] = results[pluginName];
}
return res;
}
).catch((err)=>{
logger.error(`Error searching plugins: ${err}`);
return {} as MapArrayType<PackageInfo>;
});