Files: 66ea5fb7f20bcb120716849b8b81dd19854a758b / plugins / plugins.js
8030 bytesRaw
1 | var assert = require('assert') |
2 | var path = require('path') |
3 | var fs = require('fs') |
4 | var pull = require('pull-stream') |
5 | var cat = require('pull-cat') |
6 | var many = require('pull-many') |
7 | var pushable = require('pull-pushable') |
8 | var toPull = require('stream-to-pull-stream') |
9 | var spawn = require('cross-spawn') |
10 | var mkdirp = require('mkdirp') |
11 | var osenv = require('osenv') |
12 | var rimraf = require('rimraf') |
13 | var mv = require('mv') |
14 | var mdm = require('mdmanifest') |
15 | var explain = require('explain-error') |
16 | var valid = require('../lib/validators') |
17 | var apidoc = require('../lib/apidocs').plugins |
18 | |
19 | module.exports = { |
20 | name: 'plugins', |
21 | version: '1.0.0', |
22 | manifest: mdm.manifest(apidoc), |
23 | permissions: { |
24 | master: {allow: ['install', 'uninstall', 'enable', 'disable']} |
25 | }, |
26 | init: function (server, config) { |
27 | var installPath = config.path |
28 | config.plugins = config.plugins || {} |
29 | mkdirp.sync(path.join(installPath, 'node_modules')) |
30 | |
31 | // helper to enable/disable plugins |
32 | function configPluginEnabled (b) { |
33 | return function (pluginName, cb) { |
34 | checkInstalled(pluginName, function (err) { |
35 | if (err) return cb(err) |
36 | |
37 | config.plugins[pluginName] = b |
38 | writePluginConfig(pluginName, b) |
39 | if (b) |
40 | cb(null, '\''+pluginName+'\' has been enabled. Restart Scuttlebot server to use the plugin.') |
41 | else |
42 | cb(null, '\''+pluginName+'\' has been disabled. Restart Scuttlebot server to stop using the plugin.') |
43 | }) |
44 | } |
45 | } |
46 | |
47 | // helper to check if a plugin is installed |
48 | function checkInstalled (pluginName, cb) { |
49 | if (!pluginName || typeof pluginName !== 'string') |
50 | return cb(new Error('plugin name is required')) |
51 | var modulePath = path.join(installPath, 'node_modules', pluginName) |
52 | fs.stat(modulePath, function (err) { |
53 | if (err) |
54 | cb(new Error('Plugin "'+pluginName+'" is not installed.')) |
55 | else |
56 | cb() |
57 | }) |
58 | } |
59 | |
60 | // write the plugin config to ~/.ssb/config |
61 | function writePluginConfig (pluginName, value) { |
62 | var cfgPath = path.join(config.path, 'config') |
63 | var existingConfig = {} |
64 | |
65 | // load ~/.ssb/config |
66 | try { existingConfig = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) } |
67 | catch (e) {} |
68 | |
69 | // update the plugins config |
70 | existingConfig.plugins = existingConfig.plugins || {} |
71 | existingConfig.plugins[pluginName] = value |
72 | |
73 | // write to disc |
74 | fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8') |
75 | } |
76 | |
77 | return { |
78 | install: valid.source(function (pluginName, opts) { |
79 | var p = pushable() |
80 | var dryRun = opts && opts['dry-run'] |
81 | var from = opts && opts.from |
82 | |
83 | if (!pluginName || typeof pluginName !== 'string') |
84 | return pull.error(new Error('plugin name is required')) |
85 | |
86 | // pull out the version, if given |
87 | if (pluginName.indexOf('@') !== -1) { |
88 | var pluginNameSplitted = pluginName.split('@') |
89 | pluginName = pluginNameSplitted[0] |
90 | var version = pluginNameSplitted[1] |
91 | |
92 | if (version && !from) |
93 | from = pluginName + '@' + version |
94 | } |
95 | |
96 | if (!validatePluginName(pluginName)) |
97 | return pull.error(new Error('invalid plugin name: "'+pluginName+'"')) |
98 | |
99 | // create a tmp directory to install into |
100 | var tmpInstallPath = path.join(osenv.tmpdir(), pluginName) |
101 | rimraf.sync(tmpInstallPath); mkdirp.sync(tmpInstallPath) |
102 | |
103 | // build args |
104 | // --global-style: dont dedup at the top level, gives proper isolation between each plugin |
105 | // --loglevel error: dont output warnings, because npm just whines about the lack of a package.json in ~/.ssb |
106 | var args = ['install', from||pluginName, '--global-style', '--loglevel', 'error'] |
107 | if (dryRun) |
108 | args.push('--dry-run') |
109 | |
110 | // exec npm |
111 | var child = spawn('npm', args, { cwd: tmpInstallPath }) |
112 | .on('close', function (code) { |
113 | if (code == 0 && !dryRun) { |
114 | var tmpInstallNMPath = path.join(tmpInstallPath, 'node_modules') |
115 | var finalInstallNMPath = path.join(installPath, 'node_modules') |
116 | |
117 | // delete plugin, if it's already there |
118 | rimraf.sync(path.join(finalInstallNMPath, pluginName)) |
119 | |
120 | // move the plugin from the tmpdir into our install path |
121 | // ...using our given plugin name |
122 | var dirs = fs.readdirSync(tmpInstallNMPath) |
123 | .filter(function (name) { return name.charAt(0) !== '.' }) // filter out dot dirs, like '.bin' |
124 | mv( |
125 | path.join(tmpInstallNMPath, dirs[0]), |
126 | path.join(finalInstallNMPath, pluginName), |
127 | function (err) { |
128 | if (err) |
129 | return p.end(explain(err, '"'+pluginName+'" failed to install. See log output above.')) |
130 | |
131 | // enable the plugin |
132 | // - use basename(), because plugins can be installed from the FS, in which case pluginName is a path |
133 | var name = path.basename(pluginName) |
134 | config.plugins[name] = true |
135 | writePluginConfig(name, true) |
136 | p.push(new Buffer('"'+pluginName+'" has been installed. Restart Scuttlebot server to enable the plugin.\n', 'utf-8')) |
137 | p.end() |
138 | } |
139 | ) |
140 | } else |
141 | p.end(new Error('"'+pluginName+'" failed to install. See log output above.')) |
142 | }) |
143 | return cat([ |
144 | pull.values([new Buffer('Installing "'+pluginName+'"...\n', 'utf-8')]), |
145 | many([toPull(child.stdout), toPull(child.stderr)]), |
146 | p |
147 | ]) |
148 | }, 'string', 'object?'), |
149 | uninstall: valid.source(function (pluginName, opts) { |
150 | var p = pushable() |
151 | if (!pluginName || typeof pluginName !== 'string') |
152 | return pull.error(new Error('plugin name is required')) |
153 | |
154 | var modulePath = path.join(installPath, 'node_modules', pluginName) |
155 | |
156 | rimraf(modulePath, function (err) { |
157 | if (!err) { |
158 | writePluginConfig(pluginName, false) |
159 | p.push(new Buffer('"'+pluginName+'" has been uninstalled. Restart Scuttlebot server to disable the plugin.\n', 'utf-8')) |
160 | p.end() |
161 | } else |
162 | p.end(err) |
163 | }) |
164 | return p |
165 | }, 'string', 'object?'), |
166 | enable: valid.async(configPluginEnabled(true), 'string'), |
167 | disable: valid.async(configPluginEnabled(false), 'string') |
168 | } |
169 | } |
170 | } |
171 | |
172 | module.exports.loadUserPlugins = function (createSbot, config) { |
173 | // iterate all modules |
174 | var nodeModulesPath = path.join(config.path, 'node_modules') |
175 | //instead of testing all plugins, only load things explicitly |
176 | //enabled in the config |
177 | for(var k in config.plugins) { |
178 | if(config.plugins[k]) { |
179 | try { |
180 | var plugin = require(path.join(nodeModulesPath, k)) |
181 | assertSbotPlugin(plugin) |
182 | if (createSbot.plugins.some(plug => plug.name === plugin.name)) |
183 | throw new Error('already loaded') |
184 | createSbot.use(plugin) |
185 | } catch (e) { |
186 | console.error('Error loading plugin "'+k+'":', e.message) |
187 | } |
188 | } |
189 | } |
190 | } |
191 | |
192 | // predictate to check if an object appears to be a sbot plugin |
193 | function assertSbotPlugin (obj) { |
194 | // function signature: |
195 | if (typeof obj == 'function') |
196 | return |
197 | |
198 | // object signature: |
199 | assert(obj && typeof obj == 'object', 'module.exports must be an object') |
200 | assert(typeof obj.name == 'string', 'module.exports.name must be a string') |
201 | assert(typeof obj.version == 'string', 'module.exports.version must be a string') |
202 | assert(obj.manifest && |
203 | typeof obj.manifest == 'object', 'module.exports.manifest must be an object') |
204 | assert(typeof obj.init == 'function', 'module.exports.init must be a function') |
205 | } |
206 | |
207 | function validatePluginName (name) { |
208 | if (/^[._]/.test(name)) |
209 | return false |
210 | // from npm-validate-package-name: |
211 | if (encodeURIComponent(name) !== name) |
212 | return false |
213 | return true |
214 | } |
215 | |
216 |
Built with git-ssb-web