git ssb

4+

Dominic / scuttlebot



Tree: 69dc20c646b477c874cb1f6eb145f9cd1016b04c

Files: 69dc20c646b477c874cb1f6eb145f9cd1016b04c / plugins / plugins.js

8657 bytesRaw
1var assert = require('assert')
2var path = require('path')
3var fs = require('fs')
4var pull = require('pull-stream')
5var cat = require('pull-cat')
6var many = require('pull-many')
7var pushable = require('pull-pushable')
8var toPull = require('stream-to-pull-stream')
9var spawn = require('cross-spawn')
10var mkdirp = require('mkdirp')
11var osenv = require('osenv')
12var rimraf = require('rimraf')
13var mv = require('mv')
14var mdm = require('mdmanifest')
15var explain = require('explain-error')
16var valid = require('../lib/validators')
17var apidoc = require('../lib/apidocs').plugins
18
19module.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 // load ~/.ssb/config
64 let existingConfig
65 fs.readFile(cfgPath, 'utf-8', (err, data) => {
66 if (err) {
67 if (err.code === 'ENOENT') {
68 // only catch "file not found"
69 existingConfig = {}
70 } else {
71 throw err
72 }
73 } else {
74 existingConfig = JSON.parse(data)
75 }
76
77
78 // update the plugins config
79 existingConfig.plugins = existingConfig.plugins || {}
80 existingConfig.plugins[pluginName] = value
81
82 // write to disc
83 fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8')
84 })
85
86 }
87
88 return {
89 install: valid.source(function (pluginName, opts) {
90 var p = pushable()
91 var dryRun = opts && opts['dry-run']
92 var from = opts && opts.from
93
94 if (!pluginName || typeof pluginName !== 'string')
95 return pull.error(new Error('plugin name is required'))
96
97 // pull out the version, if given
98 if (pluginName.indexOf('@') !== -1) {
99 var pluginNameSplitted = pluginName.split('@')
100 pluginName = pluginNameSplitted[0]
101 var version = pluginNameSplitted[1]
102
103 if (version && !from)
104 from = pluginName + '@' + version
105 }
106
107 if (!validatePluginName(pluginName))
108 return pull.error(new Error('invalid plugin name: "'+pluginName+'"'))
109
110 // create a tmp directory to install into
111 var tmpInstallPath = path.join(osenv.tmpdir(), pluginName)
112 rimraf.sync(tmpInstallPath); mkdirp.sync(tmpInstallPath)
113
114 // build args
115 // --global-style: dont dedup at the top level, gives proper isolation between each plugin
116 // --loglevel error: dont output warnings, because npm just whines about the lack of a package.json in ~/.ssb
117 var args = ['install', from||pluginName, '--global-style', '--loglevel', 'error']
118 if (dryRun)
119 args.push('--dry-run')
120
121 // exec npm
122 var child = spawn('npm', args, { cwd: tmpInstallPath })
123 .on('close', function (code) {
124 if (code == 0 && !dryRun) {
125 var tmpInstallNMPath = path.join(tmpInstallPath, 'node_modules')
126 var finalInstallNMPath = path.join(installPath, 'node_modules')
127
128 // delete plugin, if it's already there
129 rimraf.sync(path.join(finalInstallNMPath, pluginName))
130
131 // move the plugin from the tmpdir into our install path
132 // ...using our given plugin name
133 var dirs = fs.readdirSync(tmpInstallNMPath)
134 .filter(function (name) { return name.charAt(0) !== '.' }) // filter out dot dirs, like '.bin'
135 mv(
136 path.join(tmpInstallNMPath, dirs[0]),
137 path.join(finalInstallNMPath, pluginName),
138 function (err) {
139 if (err)
140 return p.end(explain(err, '"'+pluginName+'" failed to install. See log output above.'))
141
142 // enable the plugin
143 // - use basename(), because plugins can be installed from the FS, in which case pluginName is a path
144 var name = path.basename(pluginName)
145 config.plugins[name] = true
146 writePluginConfig(name, true)
147 p.push(Buffer.from('"'+pluginName+'" has been installed. Restart Scuttlebot server to enable the plugin.\n', 'utf-8'))
148 p.end()
149 }
150 )
151 } else
152 p.end(new Error('"'+pluginName+'" failed to install. See log output above.'))
153 })
154 return cat([
155 pull.values([Buffer.from('Installing "'+pluginName+'"...\n', 'utf-8')]),
156 many([toPull(child.stdout), toPull(child.stderr)]),
157 p
158 ])
159 }, 'string', 'object?'),
160 uninstall: valid.source(function (pluginName, opts) {
161 var p = pushable()
162 if (!pluginName || typeof pluginName !== 'string')
163 return pull.error(new Error('plugin name is required'))
164
165 var modulePath = path.join(installPath, 'node_modules', pluginName)
166
167 rimraf(modulePath, function (err) {
168 if (!err) {
169 writePluginConfig(pluginName, false)
170 p.push(Buffer.from('"'+pluginName+'" has been uninstalled. Restart Scuttlebot server to disable the plugin.\n', 'utf-8'))
171 p.end()
172 } else
173 p.end(err)
174 })
175 return p
176 }, 'string', 'object?'),
177 enable: valid.async(configPluginEnabled(true), 'string'),
178 disable: valid.async(configPluginEnabled(false), 'string')
179 }
180 }
181}
182
183module.exports.loadUserPlugins = function (createSbot, config) {
184 // iterate all modules
185 var nodeModulesPath = path.join(config.path, 'node_modules')
186 //instead of testing all plugins, only load things explicitly
187 //enabled in the config
188 for (var module_name in config.plugins) {
189 const configv = config.plugins[module_name]
190 if (configv) {
191 const name = /^ssb-/.test(module_name) ? module_name.substring(4) : module_name
192
193 if (createSbot.plugins.some(plug => plug.name === name))
194 throw new Error('already loaded plugin named:'+name)
195
196 let plugin = null
197 if (typeof configv === 'object') { // out-of-process plugin
198 plugin = require('ssb-plugins2/load')(configv.location)
199 } else if (typeof configv === 'boolean') {
200 plugin = require(path.join(nodeModulesPath, module_name))
201 }
202
203 if (!plugin || plugin.name !== name)
204 throw new Error(`plugin at:${module_name} expected name:${name} but had:${(plugin||{}).name}`)
205
206 assertSbotPlugin(plugin)
207 createSbot.use(plugin)
208 }
209 }
210}
211
212// predictate to check if an object appears to be a sbot plugin
213function assertSbotPlugin (obj) {
214 // function signature:
215 if (typeof obj == 'function')
216 return
217
218 // object signature:
219 assert(obj && typeof obj == 'object', 'module.exports must be an object')
220 assert(typeof obj.name == 'string', 'module.exports.name must be a string')
221 assert(typeof obj.version == 'string', 'module.exports.version must be a string')
222 assert(obj.manifest &&
223 typeof obj.manifest == 'object', 'module.exports.manifest must be an object')
224 assert(typeof obj.init == 'function', 'module.exports.init must be a function')
225}
226
227function validatePluginName (name) {
228 if (/^[._]/.test(name))
229 return false
230 // from npm-validate-package-name:
231 if (encodeURIComponent(name) !== name)
232 return false
233 return true
234}
235
236
237

Built with git-ssb-web