From f95d011963aa77a71779bda0a321b914f84cf324 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Tue, 17 Sep 2019 15:27:22 +0100 Subject: [PATCH] fix: fix non-disposable and normalise behaviour closes #305 closes #276 closes #354 closes #330 closes #329 --- package.json | 5 +- src/defaults/options.json | 6 -- src/endpoint/routes.js | 56 +++++----- src/factory-client.js | 32 ++++-- src/factory-daemon.js | 67 ++++-------- src/factory-in-proc.js | 84 +++++---------- src/index.js | 6 +- src/ipfsd-client.js | 200 +++++++++++++++--------------------- src/ipfsd-daemon.js | 162 +++++++++++------------------ src/ipfsd-in-proc.js | 171 ++++++++++++++---------------- src/utils/repo/browser.js | 40 +++++--- src/utils/repo/nodejs.js | 42 ++++---- test/add-retrieve.spec.js | 14 +-- test/endpoint/client.js | 26 +++-- test/endpoint/routes.js | 83 +++++---------- test/non-disposable.spec.js | 147 ++++++++++++++++++++++++++ test/spawn-options.spec.js | 192 ++++------------------------------ test/start-stop.node.js | 45 -------- 18 files changed, 574 insertions(+), 804 deletions(-) delete mode 100644 src/defaults/options.json create mode 100644 test/non-disposable.spec.js diff --git a/package.json b/package.json index 4a3ea9c2..108cc534 100644 --- a/package.json +++ b/package.json @@ -67,14 +67,11 @@ "@hapi/joi": "^15.1.1", "debug": "^4.1.1", "detect-node": "^2.0.4", - "dexie": "^2.0.4", "execa": "^2.0.4", "fs-extra": "^8.1.0", "hat": "~0.0.3", "ipfs-http-client": "^35.1.0", - "lodash.clone": "^4.5.0", - "lodash.defaults": "^4.2.0", - "lodash.defaultsdeep": "^4.6.1", + "merge-options": "^1.0.1", "multiaddr": "^7.0.0", "safe-json-stringify": "^1.2.0", "superagent": "^5.0.5" diff --git a/src/defaults/options.json b/src/defaults/options.json deleted file mode 100644 index ea8e9331..00000000 --- a/src/defaults/options.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "go", - "disposable": true, - "start": true, - "init": true -} diff --git a/src/endpoint/routes.js b/src/endpoint/routes.js index 505caf3d..13b58423 100644 --- a/src/endpoint/routes.js +++ b/src/endpoint/routes.js @@ -3,7 +3,7 @@ const hat = require('hat') const Joi = require('@hapi/joi') const boom = require('@hapi/boom') -const defaults = require('lodash.defaultsdeep') +const merge = require('merge-options') const FactoryDaemon = require('../factory-daemon') const tmpDir = require('../utils/tmp-dir') @@ -27,11 +27,13 @@ module.exports = (server) => { server.route({ method: 'GET', path: '/util/tmp-dir', - handler: (request) => { + handler: async (request) => { const type = request.query.type || 'go' - const path = tmpDir(type === 'js') - - return { tmpDir: path } + try { + return { tmpDir: await tmpDir(type === 'js') } + } catch (err) { + throw boom.badRequest(err.message) + } } }) @@ -68,28 +70,18 @@ module.exports = (server) => { const f = new FactoryDaemon({ type: payload.type }) try { - const ipfsd = await f.spawn(payload.options) + const ipfsd = await f.spawn(payload) const id = hat() - const initialized = ipfsd.initialized nodes[id] = ipfsd - let api = null - - if (nodes[id].started) { - api = { - apiAddr: nodes[id].apiAddr - ? nodes[id].apiAddr.toString() - : '', - gatewayAddr: nodes[id].gatewayAddr - ? nodes[id].gatewayAddr.toString() - : '' - } - } - return { - id, - api, - initialized + _id: id, + apiAddr: ipfsd.apiAddr ? ipfsd.apiAddr.toString() : '', + gatewayAddr: ipfsd.gatewayAddr ? ipfsd.gatewayAddr.toString() : '', + initialized: ipfsd.initialized, + started: ipfsd.started, + _env: ipfsd._env, + path: ipfsd.path } } catch (err) { throw boom.badRequest(err.message) @@ -135,10 +127,8 @@ module.exports = (server) => { await nodes[id].start(flags) return { - api: { - apiAddr: nodes[id].apiAddr.toString(), - gatewayAddr: nodes[id].gatewayAddr.toString() - } + apiAddr: nodes[id].apiAddr.toString(), + gatewayAddr: nodes[id].gatewayAddr ? nodes[id].gatewayAddr.toString() : '' } } catch (err) { throw boom.badRequest(err.message) @@ -206,7 +196,7 @@ module.exports = (server) => { path: '/stop', handler: async (request, h) => { const id = request.query.id - const timeout = request.payload.timeout + const timeout = request.payload && request.payload.timeout try { await nodes[id].stop(timeout) @@ -230,7 +220,7 @@ module.exports = (server) => { path: '/kill', handler: async (request, h) => { const id = request.query.id - const timeout = request.payload.timeout + const timeout = request.payload && request.payload.timeout try { await nodes[id].killProcess(timeout) @@ -277,13 +267,13 @@ module.exports = (server) => { throw boom.badRequest(err.message) } }, - config: defaults({}, { + config: merge(routeConfig, { validate: { query: { key: Joi.string().optional() } } - }, routeConfig) + }) }) /* @@ -305,13 +295,13 @@ module.exports = (server) => { return h.response().code(200) }, - config: defaults({}, { + config: merge(routeConfig, { validate: { payload: { key: Joi.string(), value: Joi.any() } } - }, routeConfig) + }) }) } diff --git a/src/factory-client.js b/src/factory-client.js index bfa9236c..b6726674 100644 --- a/src/factory-client.js +++ b/src/factory-client.js @@ -2,6 +2,8 @@ const request = require('superagent') const DaemonClient = require('./ipfsd-client') +const merge = require('merge-options') +const defaultConfig = require('./defaults/config.json') /** @ignore @typedef {import("./index").SpawnOptions} SpawnOptions */ @@ -33,12 +35,14 @@ class FactoryClient { * Utility method to get a temporary directory * useful in browsers to be able to generate temp * repos manually + * @param {boolean} isJS * * @returns {Promise} */ - async tmpDir () { + async tmpDir (isJS) { const res = await request .get(`${this.baseUrl}/util/tmp-dir`) + .query({ type: isJS ? 'js' : 'go' }) return res.body.tmpDir } @@ -66,20 +70,28 @@ class FactoryClient { * @return {Promise} */ async spawn (options = {}) { + const daemonOptions = merge({ + exec: this.options.exec, + type: this.options.type, + IpfsClient: this.options.IpfsClient, + disposable: true, + start: options.disposable !== false, + init: options.disposable !== false, + config: defaultConfig + }, options) + + if (options.defaultAddrs) { + delete daemonOptions.config.Addresses + } + const res = await request .post(`${this.baseUrl}/spawn`) - .send({ options: options, type: this.options.type }) - - const apiAddr = res.body.api ? res.body.api.apiAddr : '' - const gatewayAddr = res.body.api ? res.body.api.gatewayAddr : '' + .send(daemonOptions) const ipfsd = new DaemonClient( this.baseUrl, - res.body.id, - res.body.initialized, - apiAddr, - gatewayAddr, - { IpfsClient: this.options.IpfsClient } + res.body, + daemonOptions ) return ipfsd diff --git a/src/factory-daemon.js b/src/factory-daemon.js index 13f386f1..d92ebeab 100644 --- a/src/factory-daemon.js +++ b/src/factory-daemon.js @@ -1,12 +1,9 @@ 'use strict' -const defaultsDeep = require('lodash.defaultsdeep') -const clone = require('lodash.clone') -const path = require('path') const tmpDir = require('./utils/tmp-dir') const Daemon = require('./ipfsd-daemon') +const merge = require('merge-options') const defaultConfig = require('./defaults/config.json') -const defaultOptions = require('./defaults/options.json') /** @ignore @typedef {import("./index").SpawnOptions} SpawnOptions */ @@ -22,7 +19,7 @@ class FactoryDaemon { if (options && options.type === 'proc') { throw new Error('This Factory does not know how to spawn in proc nodes') } - this.options = Object.assign({}, { type: 'go' }, options) + this.options = merge({ type: 'go' }, options) } /** @@ -36,7 +33,7 @@ class FactoryDaemon { * @returns {Promise} */ tmpDir (type) { - return tmpDir(type === 'js') + return Promise.resolve(tmpDir(type === 'js')) } /** @@ -68,57 +65,31 @@ class FactoryDaemon { * Spawn an IPFS node, either js-ipfs or go-ipfs * * @param {SpawnOptions} [options={}] - Various config options and ipfs config parameters - * @param {function(Error, Daemon): void} callback - Callback receives Error or a Daemon instance, Daemon has a `api` property which is an `ipfs-http-client` instance. - * @returns {void} + * @returns {Promise} */ async spawn (options = {}) { - // TODO this options parsing is daunting. Refactor and move to a separate - // func documenting what it is trying to do. - options = defaultsDeep( - { IpfsClient: this.options.IpfsClient }, - options, - defaultOptions - ) - - options.init = typeof options.init !== 'undefined' - ? options.init - : true - - if (!options.disposable) { - const nonDisposableConfig = clone(defaultConfig) - options.init = false - options.start = false - - const defaultRepo = path.join( - process.env.HOME || process.env.USERPROFILE, - options.isJs - ? '.jsipfs' - : '.ipfs' - ) - - options.repoPath = options.repoPath || - (process.env.IPFS_PATH || defaultRepo) - options.config = defaultsDeep({}, options.config, nonDisposableConfig) - } else { - options.config = defaultsDeep({}, options.config, defaultConfig) - } + const daemonOptions = merge({ + exec: this.options.exec, + type: this.options.type, + IpfsClient: this.options.IpfsClient, + disposable: true, + start: options.disposable !== false, + init: options.disposable !== false, + config: defaultConfig + }, options) if (options.defaultAddrs) { - delete options.config.Addresses + delete daemonOptions.config.Addresses } - options.type = this.options.type - options.exec = options.exec || this.options.exec - options.initOptions = defaultsDeep({}, this.options.initOptions, options.initOptions) - - const node = new Daemon(options) + const node = new Daemon(daemonOptions) - if (options.init) { - await node.init(options.initOptions) + if (daemonOptions.init) { + await node.init(daemonOptions.initOptions) } - if (options.start) { - await node.start(options.args) + if (daemonOptions.start) { + await node.start(daemonOptions.args) } return node diff --git a/src/factory-in-proc.js b/src/factory-in-proc.js index 78b6ba89..a864353a 100644 --- a/src/factory-in-proc.js +++ b/src/factory-in-proc.js @@ -1,13 +1,9 @@ 'use strict' -const defaults = require('lodash.defaultsdeep') -const clone = require('lodash.clone') -const path = require('path') const tmpDir = require('./utils/tmp-dir') -const repoUtils = require('./utils/repo/nodejs') const InProc = require('./ipfsd-in-proc') +const merge = require('merge-options') const defaultConfig = require('./defaults/config.json') -const defaultOptions = require('./defaults/options.json') /** @ignore @typedef {import("./index").SpawnOptions} SpawnOptions */ @@ -37,7 +33,7 @@ class FactoryInProc { * @returns {Promise} */ tmpDir () { - return tmpDir(true) + return Promise.resolve(tmpDir(true)) } /** @@ -46,74 +42,46 @@ class FactoryInProc { * @param {Object} [options={}] * @returns {Promise} */ - version (options = {}) { - return new Promise((resolve, reject) => { - const node = new InProc(options) - node.once('ready', () => { - node.version() - .then(resolve, reject) - }) - node.once('error', reject) - }) + async version (options = {}) { + const node = new InProc(options) + const v = await node.version() + return v } /** * Spawn JSIPFS instances * - * @param {SpawnOptions} [opts={}] - various config options and ipfs config parameters - * @returns {Promise} - Resolves to an array with an `ipfs-instance` attached to the node and a `Node` + * @param {SpawnOptions} [options={}] - various config options and ipfs config parameters + * @returns {Promise} - Resolves to an array with an `ipfs-instance` attached to the node and a `Node` */ - async spawn (opts = {}) { - const options = defaults({}, opts, defaultOptions) - options.init = typeof options.init !== 'undefined' - ? options.init - : true - - if (options.disposable) { - options.config = defaults({}, options.config, defaultConfig) - } else { - const nonDisposableConfig = clone(defaultConfig) - options.init = false - options.start = false - - const defaultRepo = path.join( - process.env.HOME || process.env.USERPROFILE || '', - options.isJs ? '.jsipfs' : '.ipfs' - ) - - options.repoPath = options.repoPath || (process.env.IPFS_PATH || defaultRepo) - options.config = defaults({}, options.config, nonDisposableConfig) - } + async spawn (options = {}) { + const daemonOptions = merge({ + exec: this.options.exec, + type: this.options.type, + IpfsApi: this.options.IpfsApi, + disposable: true, + start: options.disposable !== false, + init: options.disposable !== false, + config: defaultConfig, + silent: true + }, options) if (options.defaultAddrs) { - delete options.config.Addresses + delete daemonOptions.config.Addresses } - options.type = this.options.type - options.exec = options.exec || this.options.exec - options.silent = options.silent || true - - if (typeof options.exec !== 'function') { + if (typeof this.options.exec !== 'function') { throw new Error(`'type' proc requires 'exec' to be a coderef`) } - const node = new InProc(options) - - await new Promise((resolve, reject) => { - node.once('error', reject) - node.once('ready', () => { - resolve() - }) - }) - - node.initialized = await repoUtils.repoExists(node.path) + const node = new InProc(daemonOptions) - if (options.init) { - await node.init() + if (daemonOptions.init) { + await node.init(daemonOptions.initOptions) } - if (options.start) { - await node.start(options.args) + if (daemonOptions.start) { + await node.start(daemonOptions.args) } return node diff --git a/src/index.js b/src/index.js index dd661777..5e044d31 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ 'use strict' const isNode = require('detect-node') -const defaults = require('lodash.defaultsdeep') +const merge = require('merge-options') const FactoryDaemon = require('./factory-daemon') const FactoryInProc = require('./factory-in-proc') @@ -23,10 +23,10 @@ const Server = require('./endpoint/server') * - js - spawn js-ipfs daemon * - proc - spawn in-process js-ipfs instance. Needs to be called also with exec. Example: `IPFSFactory.create({type: 'proc', exec: require('ipfs') })`. * @param {Object} IpfsClient - A custom IPFS API constructor to use instead of the packaged one `js-ipfs-http-client`. - * @returns {(FactoryDaemon|FactoryClient|FactoryInProc)} + * @returns {(FactoryDaemon|FactoryInProc|FactoryClient)} */ const create = (opts) => { - const options = defaults({}, opts, { remote: !isNode }) + const options = merge({ remote: !isNode }, opts) if (options.type === 'proc') { return new FactoryInProc(options) diff --git a/src/ipfsd-client.js b/src/ipfsd-client.js index 3f557c61..4e1ea904 100644 --- a/src/ipfsd-client.js +++ b/src/ipfsd-client.js @@ -4,113 +4,74 @@ const request = require('superagent') const IpfsClient = require('ipfs-http-client') const multiaddr = require('multiaddr') -function createApi (apiAddr, gwAddr, IpfsClient) { - let api - if (apiAddr) { - api = IpfsClient(apiAddr) - api.apiHost = multiaddr(apiAddr).nodeAddress().address - api.apiPort = multiaddr(apiAddr).nodeAddress().port - } - - if (api && gwAddr) { - api.gatewayHost = multiaddr(gwAddr).nodeAddress().address - api.gatewayPort = multiaddr(gwAddr).nodeAddress().port - } - - return api -} - -function translateError (err) { - let message = err.message - - if (err.response && err.response.body && err.response.body.message) { - message = err.response.body.message - } - - const output = new Error(message) - output.status = err.status - output.response = err.response - output.stack = err.stack - output.message = message - - throw output -} - /** * Creates an instance of Client. * * @param {*} baseUrl * @param {*} _id * @param {*} initialized - * @param {*} apiAddr - * @param {*} gwAddrs * @param {*} options */ class Client { - constructor (baseUrl, _id, initialized, apiAddr, gwAddrs, options) { - this.options = options || {} + constructor (baseUrl, remoteState, options = {}) { + this.options = options this.baseUrl = baseUrl - this._id = _id - this._apiAddr = multiaddr(apiAddr) - this._gwAddr = multiaddr(gwAddrs) - this.initialized = initialized - this.started = false - this.api = createApi(apiAddr, gwAddrs, this.options.IpfsClient || IpfsClient) + this._id = remoteState._id + this.path = remoteState.path + this.initialized = remoteState.initialized + this.started = remoteState.started + this.clean = true + this.apiAddr = null + this.gatewayAddr = null + this.api = null + + if (this.started) { + this.setApi(remoteState.apiAddr) + this.setGateway(remoteState.gatewayAddr) + } } - /** - * Get the address of connected IPFS API. - * - * @returns {Multiaddr} - */ - get apiAddr () { - return this._apiAddr + setApi (addr) { + if (addr) { + this.apiAddr = multiaddr(addr) + this.api = (this.options.IpfsClient || IpfsClient)(addr) + this.api.apiHost = this.apiAddr.nodeAddress().address + this.api.apiPort = this.apiAddr.nodeAddress().port + } } - /** - * Set the address of connected IPFS API. - * - * @param {Multiaddr} addr - * @returns {void} - */ - set apiAddr (addr) { - this._apiAddr = addr + setGateway (addr) { + if (addr) { + this.gatewayAddr = multiaddr(addr) + this.api.gatewayHost = this.gatewayAddr.nodeAddress().address + this.api.gatewayPort = this.gatewayAddr.nodeAddress().port + } } /** - * Get the address of connected IPFS HTTP Gateway. + * Initialize a repo. * - * @returns {Multiaddr} + * @param {Object} [initOptions] + * @param {number} [initOptions.keysize=2048] - The bit size of the identiy key. + * @param {string} [initOptions.directory=IPFS_PATH] - The location of the repo. + * @returns {Promise} */ - get gatewayAddr () { - return this._gwAddr - } + async init (initOptions) { + if (this.initialized && initOptions) { + throw new Error(`Repo already initialized can't use different options, ${JSON.stringify(initOptions)}`) + } + if (this.initialized) { + this.clean = false + return this + } - /** - * Set the address of connected IPFS Gateway. - * - * @param {Multiaddr} addr - * @returns {void} - */ - set gatewayAddr (addr) { - this._gwAddr = addr - } + initOptions = initOptions || {} - /** - * Initialize a repo. - * - * @param {Object} [initOpts={}] - * @param {number} [initOpts.keysize=2048] - The bit size of the identiy key. - * @param {string} [initOpts.directory=IPFS_PATH] - The location of the repo. - * @param {function (Error, Node)} cb - * @returns {undefined} - */ - async init (initOpts = {}) { + // TODO probably needs to change config like the other impl const res = await request .post(`${this.baseUrl}/init`) .query({ id: this._id }) - .send({ initOpts }) - .catch(translateError) + .send({ initOptions }) this.initialized = res.body.initialized @@ -122,36 +83,47 @@ class Client { * If the node was marked as `disposable` this will be called * automatically when the process is exited. * - * @param {function(Error)} cb - * @returns {undefined} + * @returns {Promise} */ - cleanup () { - return request + async cleanup () { + if (this.clean) { + return this + } + + await request .post(`${this.baseUrl}/cleanup`) .query({ id: this._id }) - .catch(translateError) + + this.clean = true } /** * Start the daemon. * * @param {Array} [flags=[]] - Flags to be passed to the `ipfs daemon` command. - * @param {function(Error, IpfsClient)} cb - * @returns {undefined} + * @returns {Promise} */ async start (flags = []) { + if (this.started) { + return this.api + } const res = await request .post(`${this.baseUrl}/start`) .query({ id: this._id }) .send({ flags }) - .catch(translateError) this.started = true - const apiAddr = res.body.api ? res.body.api.apiAddr : '' - const gatewayAddr = res.body.api ? res.body.api.gatewayAddr : '' + const apiAddr = res.body ? res.body.apiAddr : '' + const gatewayAddr = res.body ? res.body.gatewayAddr : '' - this.api = createApi(apiAddr, gatewayAddr, this.options.IpfsClient || IpfsClient) + if (apiAddr) { + this.setApi(apiAddr) + } + + if (gatewayAddr) { + this.setGateway(gatewayAddr) + } return this.api } @@ -159,18 +131,20 @@ class Client { /** * Stop the daemon. * - * @param {integer|undefined} timeout - Grace period to wait before force stopping the node - * @param {function(Error)} [cb] - * @returns {undefined} + * @param {number} [timeout] - Grace period to wait before force stopping the node + * @returns {Promise} */ async stop (timeout) { + if (!this.started) { + return this + } await request .post(`${this.baseUrl}/stop`) .query({ id: this._id }) .send({ timeout }) - .catch(translateError) - this.started = false + + return this } /** @@ -179,31 +153,29 @@ class Client { * First `SIGTERM` is sent, after 10.5 seconds `SIGKILL` is sent * if the process hasn't exited yet. * - * @param {integer|undefined} timeout - Grace period to wait before force stopping the node - * @param {function()} [cb] - Called when the process was killed. - * @returns {undefined} + * @param {number} [timeout] - Grace period to wait before force stopping the node + * @returns {Promise} */ async killProcess (timeout) { await request .post(`${this.baseUrl}/kill`) .query({ id: this._id }) .send({ timeout }) - .catch(translateError) this.started = false + + return this } /** * Get the pid of the `ipfs daemon` process. * - * @param {function(Error, number): void} cb - receives the pid - * @returns {void} + * @returns {Promise} */ async pid () { const res = await request .get(`${this.baseUrl}/pid`) .query({ id: this._id }) - .catch(translateError) return res.body.pid } @@ -214,20 +186,12 @@ class Client { * If no `key` is passed, the whole config is returned as an object. * * @param {string} [key] - A specific config to retrieve. - * @param {function(Error, (Object|string))} cb - * @returns {void} + * @returns {Promise} */ async getConfig (key) { - const qr = { id: this._id } - - if (key) { - qr.key = key - } - const res = await request .get(`${this.baseUrl}/config`) - .query(qr) - .catch(translateError) + .query({ id: this._id, key }) return res.body.config } @@ -237,14 +201,12 @@ class Client { * * @param {string} key * @param {string} value - * @param {function(Error)} cb - * @returns {void} + * @returns {Promise} */ async setConfig (key, value) { await request.put(`${this.baseUrl}/config`) .send({ key, value }) .query({ id: this._id }) - .catch(translateError) } } diff --git a/src/ipfsd-daemon.js b/src/ipfsd-daemon.js index c5e51dff..0745ca6c 100644 --- a/src/ipfsd-daemon.js +++ b/src/ipfsd-daemon.js @@ -4,23 +4,22 @@ const IpfsClient = require('ipfs-http-client') const multiaddr = require('multiaddr') const fs = require('fs-extra') const path = require('path') -const defaults = require('lodash.defaults') +const merge = require('merge-options') const debug = require('debug') const os = require('os') const hat = require('hat') const log = debug('ipfsd-ctl:daemon') -const daemonLog = { - info: debug('ipfsd-ctl:daemon:stdout'), - err: debug('ipfsd-ctl:daemon:stderr') -} - const safeStringify = require('safe-json-stringify') - const tmpDir = require('./utils/tmp-dir') const findIpfsExecutable = require('./utils/find-ipfs-executable') const setConfigValue = require('./utils/set-config-value') const run = require('./utils/run') +const { checkForRunningApi, defaultRepo } = require('./utils/repo/nodejs') +const daemonLog = { + info: debug('ipfsd-ctl:daemon:stdout'), + err: debug('ipfsd-ctl:daemon:stderr') +} // amount of ms to wait before sigkill const GRACE_PERIOD = 10500 @@ -44,76 +43,41 @@ function translateError (err) { * @param {Typedefs.SpawnOptions} [opts] */ class Daemon { - constructor (opts) { + constructor (opts = { type: 'go' }) { const rootPath = process.env.testpath ? process.env.testpath : __dirname - this.opts = opts || { type: 'go' } - const td = tmpDir(this.opts.type === 'js') - this.path = this.opts.disposable - ? td - : (this.opts.repoPath || td) - this.disposable = this.opts.disposable - - if (process.env.IPFS_EXEC) { - log('WARNING: The use of IPFS_EXEC is deprecated, ' + - 'please use IPFS_GO_EXEC or IPFS_JS_EXEC respectively!') - - if (this.opts.type === 'go') { - process.env.IPFS_GO_EXEC = process.env.IPFS_EXEC - } else { - process.env.IPFS_JS_EXEC = process.env.IPFS_EXEC - } - - delete process.env.IPFS_EXEC - } - + this.opts = opts const envExec = this.opts.type === 'go' ? process.env.IPFS_GO_EXEC : process.env.IPFS_JS_EXEC this.exec = this.opts.exec || envExec || findIpfsExecutable(this.opts.type, rootPath) + this._env = Object.assign({}, process.env, this.opts.env) + this.path = this.opts.disposable + ? tmpDir(this.opts.type === 'js') + : (this.opts.repoPath || defaultRepo(this.opts.type)) + this.bits = this.opts.initOptions ? this.opts.initOptions.bits : null + this.disposable = this.opts.disposable this.subprocess = null this.initialized = fs.existsSync(this.path) + this.started = false this.clean = true - this._apiAddr = null - this._gatewayAddr = null - this._started = false + this.apiAddr = null + this.gatewayAddr = null /** @member {IpfsClient} */ this.api = null - this.bits = this.opts.initOptions ? this.opts.initOptions.bits : null - this._env = Object.assign({}, process.env, this.opts.env) } - /** - * Running node api - * @member {String} - */ - get runningNodeApi () { - let api - try { - api = fs.readFileSync(`${this.repoPath}/api`) - } catch (err) { - log(`Unable to open api file: ${err}`) - } - - return api ? api.toString() : null + setApi (addr) { + this.apiAddr = multiaddr(addr) + this.api = (this.opts.IpfsClient || IpfsClient)(addr) + this.api.apiHost = this.apiAddr.nodeAddress().address + this.api.apiPort = this.apiAddr.nodeAddress().port } - /** - * Address of connected IPFS API. - * - * @member {Multiaddr} - */ - get apiAddr () { - return this._apiAddr - } - - /** - * Address of connected IPFS HTTP Gateway. - * - * @member {Multiaddr} - */ - get gatewayAddr () { - return this._gatewayAddr + setGateway (addr) { + this.gatewayAddr = multiaddr(addr) + this.api.gatewayHost = this.gatewayAddr.nodeAddress().address + this.api.gatewayPort = this.gatewayAddr.nodeAddress().port } /** @@ -125,15 +89,6 @@ class Daemon { return this.path } - /** - * Is the node started - * - * @member {boolean} - */ - get started () { - return this._started - } - /** * Shell environment variables * @@ -152,7 +107,18 @@ class Daemon { * @param {string} [initOptions.pass] - The passphrase of the keychain. * @returns {Promise} */ - async init (initOptions = {}) { + async init (initOptions) { + if (this.initialized && initOptions) { + throw new Error(`Repo already initialized can't use different options, ${JSON.stringify(initOptions)}`) + } + + if (this.initialized) { + this.clean = false + return this + } + + initOptions = initOptions || {} + if (initOptions.directory && initOptions.directory !== this.path) { this.path = initOptions.directory } @@ -182,10 +148,9 @@ class Daemon { await run(this, args, { env: this.env }) .catch(translateError) - const conf = await this.getConfig() - await this.replaceConfig(defaults({}, this.opts.config, conf)) + await this.replaceConfig(merge(conf, this.opts.config)) this.clean = false this.initialized = true @@ -200,12 +165,11 @@ class Daemon { */ async cleanup () { if (this.clean) { - return + return this } - this.clean = true - await fs.remove(this.path) + this.clean = true } /** @@ -216,25 +180,12 @@ class Daemon { */ start (flags = []) { const args = ['daemon'].concat(flags || []) - - const setApiAddr = (addr) => { - this._apiAddr = multiaddr(addr) - this.api = (this.opts.IpfsClient || IpfsClient)(addr) - this.api.apiHost = this.apiAddr.nodeAddress().address - this.api.apiPort = this.apiAddr.nodeAddress().port - } - - const setGatewayAddr = (addr) => { - this._gatewayAddr = multiaddr(addr) - this.api.gatewayHost = this.gatewayAddr.nodeAddress().address - this.api.gatewayPort = this.gatewayAddr.nodeAddress().port - } - - const api = this.runningNodeApi + // Check if a daemon is already running + const api = checkForRunningApi(this.path) if (api) { - setApiAddr(api) - this._started = true + this.setApi(api) + this.started = true return this.api } @@ -263,16 +214,16 @@ class Daemon { const gwMatch = output.trim().match(/Gateway .*listening on:? (.*)/) if (apiMatch && apiMatch.length > 0) { - setApiAddr(apiMatch[1]) + this.setApi(apiMatch[1]) } if (gwMatch && gwMatch.length > 0) { - setGatewayAddr(gwMatch[1]) + this.setGateway(gwMatch[1]) } if (output.match(/(?:daemon is running|Daemon is ready)/)) { // we're good - this._started = true + this.started = true resolve(this.api) } } @@ -294,11 +245,19 @@ class Daemon { * @return {Promise} */ async stop (timeout) { + if (!this.started) { + return this + } if (!this.subprocess) { - return + return this } - + await this.api.stop() + // TODO this should call this.api.stop await this.killProcess(timeout) + + if (this.disposable) { + return this.cleanup() + } } /** @@ -333,7 +292,7 @@ class Daemon { subprocess.once('exit', () => { log('killed', subprocess.pid) clearTimeout(grace) - this._started = false + this.started = false if (this.disposable) { return this.cleanup().then(resolve, reject) @@ -405,9 +364,6 @@ class Daemon { async replaceConfig (config) { const tmpFile = path.join(os.tmpdir(), hat()) - // TODO: we're using tmp file here until - // https://github.com/ipfs/js-ipfs/pull/785 - // is ready await fs.writeFile(tmpFile, safeStringify(config)) await run(this, ['config', 'replace', `${tmpFile}`], { env: this.env }) .catch(translateError) diff --git a/src/ipfsd-in-proc.js b/src/ipfsd-in-proc.js index 5deb68be..0daa3771 100644 --- a/src/ipfsd-in-proc.js +++ b/src/ipfsd-in-proc.js @@ -1,15 +1,10 @@ 'use strict' const multiaddr = require('multiaddr') -const defaultsDeep = require('lodash.defaultsdeep') -const defaults = require('lodash.defaults') -const debug = require('debug') -const EventEmitter = require('events') -const repoUtils = require('./utils/repo/nodejs') - -const log = debug('ipfsd-ctl:in-proc') - -let IPFS = null +const IpfsClient = require('ipfs-http-client') +const merge = require('merge-options') +const tmpDir = require('./utils/tmp-dir') +const { repoExists, removeRepo, checkForRunningApi, defaultRepo } = require('./utils/repo/nodejs') /** * ipfsd for a js-ipfs instance (aka in-process IPFS node) @@ -17,27 +12,23 @@ let IPFS = null * @param {Object} [opts] * @param {Object} [opts.env={}] - Additional environment settings, passed to executing shell. */ -class InProc extends EventEmitter { - constructor (opts) { - super() - this.opts = opts || {} - - IPFS = this.opts.exec - +class InProc { + constructor (opts = {}) { + this.opts = opts this.opts.args = this.opts.args || [] - this.path = this.opts.repoPath || repoUtils.createTempRepoPath() + this.path = this.opts.disposable + ? tmpDir(this.opts.type === 'js') + : (this.opts.repoPath || defaultRepo(this.opts.type)) + this.bits = this.opts.initOptions ? this.opts.initOptions.bits : null this.disposable = this.opts.disposable + this.initialized = false + this.started = false this.clean = true - this._apiAddr = null - this._gatewayAddr = null - this._started = false + this.apiAddr = null + this.gatewayAddr = null this.api = null - this.initialized = false - this.bits = this.opts.initOptions ? this.opts.initOptions.bits : null - this.opts.EXPERIMENTAL = defaultsDeep({}, opts.EXPERIMENTAL, { - sharding: false - }) + this.opts.EXPERIMENTAL = merge({ sharding: false }, opts.EXPERIMENTAL) this.opts.args.forEach((arg) => { if (arg === '--enable-sharding-experiment') { @@ -54,42 +45,39 @@ class InProc extends EventEmitter { throw new Error(`Unknown argument ${arg}`) } }) + } + + async setExec () { + if (this.api !== null) { + return this + } - this.exec = new IPFS({ + const IPFS = this.opts.exec + + this.api = await IPFS.create({ repo: this.path, init: false, start: false, pass: this.opts.pass, - offline: this.opts.offline, EXPERIMENTAL: this.opts.EXPERIMENTAL, libp2p: this.opts.libp2p, config: this.opts.config, - relay: this.opts.relay, silent: this.opts.silent }) - - // TODO: should this be wrapped in a process.nextTick(), for context: - // https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#why-use-process-nexttick - this.exec.once('error', err => this.emit('error', err)) - this.exec.once('ready', () => this.emit('ready')) + return this } - /** - * Get the address of connected IPFS API. - * - * @member {Multiaddr} - */ - get apiAddr () { - return this._apiAddr + setApi (addr) { + this.apiAddr = multiaddr(addr) + this.api = (this.opts.IpfsApi || IpfsClient)(addr) + this.api.apiHost = this.apiAddr.nodeAddress().address + this.api.apiPort = this.apiAddr.nodeAddress().port } - /** - * Get the address of connected IPFS HTTP Gateway. - * - * @member {Multiaddr} - */ - get gatewayAddr () { - return this._gatewayAddr + setGateway (addr) { + this.gatewayAddr = multiaddr(addr) + this.api.gatewayHost = this.gatewayAddr.nodeAddress().address + this.api.gatewayPort = this.gatewayAddr.nodeAddress().port } /** @@ -101,15 +89,6 @@ class InProc extends EventEmitter { return this.path } - /** - * Is the node started - * - * @member {boolean} - */ - get started () { - return this._started - } - /** * Is the environment * @@ -128,26 +107,28 @@ class InProc extends EventEmitter { * @param {string} [initOptions.pass] - The passphrase of the keychain. * @returns {Promise} */ - async init (initOptions = {}) { - const bits = initOptions.keysize ? initOptions.bits : this.bits - // do not just set a default keysize, - // in case we decide to change it at - // the daemon level in the future - if (bits) { - initOptions.bits = bits - log(`initializing with keysize: ${bits}`) + async init (initOptions) { + const initialized = await repoExists(this.path) + if (initialized && initOptions) { + throw new Error(`Repo already initialized can't use different options, ${JSON.stringify(initOptions)}`) } - await this.exec.init(initOptions) - - const self = this - const conf = await this.getConfig() + if (initialized) { + this.initialized = true + this.clean = false + return this + } - await this.replaceConfig(defaults({}, this.opts.config, conf)) + // Repo not initialized + initOptions = initOptions || {} - self.clean = false - self.initialized = true + await this.setExec() + await this.api.init(initOptions) + const conf = await this.getConfig() + await this.replaceConfig(merge(conf, this.opts.config)) + this.clean = false + this.initialized = true return this } @@ -158,12 +139,12 @@ class InProc extends EventEmitter { * * @returns {Promise} */ - cleanup () { + async cleanup () { if (this.clean) { - return + return this } - - return repoUtils.removeRepo(this.path) + await removeRepo(this.path) + this.clean = true } /** @@ -172,18 +153,17 @@ class InProc extends EventEmitter { * @returns {Promise} */ async start () { - await this.exec.start() - - this._started = true - this.api = this.exec - - const conf = await this.exec.config.get() - - this._apiAddr = conf.Addresses.API - this._gatewayAddr = conf.Addresses.Gateway + // Check if a daemon is already running + const api = checkForRunningApi(this.path) + if (api) { + this.setApi(api) + this.started = true + return this.api + } - this.api.apiHost = multiaddr(conf.Addresses.API).nodeAddress().host - this.api.apiPort = multiaddr(conf.Addresses.API).nodeAddress().port + await this.setExec() + await this.api.start() + this.started = true return this.api } @@ -194,13 +174,13 @@ class InProc extends EventEmitter { * @returns {Promise} */ async stop () { - if (!this.exec) { - return + if (!this.api || !this.started) { + return this } - await this.exec.stop() + await this.api.stop() - this._started = false + this.started = false if (this.disposable) { return this.cleanup() @@ -237,7 +217,7 @@ class InProc extends EventEmitter { * @returns {Promise} */ getConfig (key) { - return this.exec.config.get(key) + return this.api.config.get(key) } /** @@ -248,7 +228,7 @@ class InProc extends EventEmitter { * @returns {Promise} */ setConfig (key, value) { - return this.exec.config.set(key, value) + return this.api.config.set(key, value) } /** @@ -258,7 +238,7 @@ class InProc extends EventEmitter { * @return {Promise} */ replaceConfig (config) { - return this.exec.config.replace(config) + return this.api.config.replace(config) } /** @@ -266,8 +246,9 @@ class InProc extends EventEmitter { * * @returns {Promise} */ - version () { - return this.exec.version() + async version () { + await this.setExec() + return this.api.version() } } diff --git a/src/utils/repo/browser.js b/src/utils/repo/browser.js index 6a448e89..e1788e71 100644 --- a/src/utils/repo/browser.js +++ b/src/utils/repo/browser.js @@ -1,27 +1,37 @@ 'use strict' -const hat = require('hat') -const Dexie = require('dexie').default - -function createTempRepoPath () { - return '/ipfs-' + hat() +function removeRepo (repoPath) { + self.indexedDB.deleteDatabase(repoPath) + return Promise.resolve() } -function removeRepo (repoPath) { - Dexie.delete(repoPath) +function repoExists (repoPath) { + return new Promise((resolve, reject) => { + var req = self.indexedDB.open(repoPath) + var existed = true + req.onerror = () => reject(req.error) + req.onsuccess = function () { + req.result.close() + if (!existed) { self.indexedDB.deleteDatabase(repoPath) } + resolve(existed) + } + req.onupgradeneeded = function () { + existed = false + } + }) } -async function repoExists (repoPath) { - const db = new Dexie(repoPath) - const store = await db.open(repoPath) - const table = store.table(repoPath) - const count = await table.count() +function defaultRepo (type) { + return 'ipfs' +} - return count > 0 +function checkForRunningApi (path) { + return null } module.exports = { - createTempRepoPath, removeRepo, - repoExists + repoExists, + defaultRepo, + checkForRunningApi } diff --git a/src/utils/repo/nodejs.js b/src/utils/repo/nodejs.js index 082b86ec..0fdaeb11 100644 --- a/src/utils/repo/nodejs.js +++ b/src/utils/repo/nodejs.js @@ -2,38 +2,44 @@ const os = require('os') const path = require('path') -const hat = require('hat') const fs = require('fs-extra') +const debug = require('debug') +const log = debug('ipfsd-ctl') -function removeRepo (dir) { +const removeRepo = async (dir) => { try { - return fs.remove(dir) + await fs.remove(dir) } catch (err) { - if (err.code === 'ENOENT') { - // Does not exist so all good - return - } - - throw err + // ignore } } -function createTempRepoPath () { - return path.join(os.tmpdir(), '/ipfs-test-' + hat()) +const repoExists = async (repoPath) => { + const exists = await fs.pathExists(`${repoPath}/config`) + return exists } -async function repoExists (repoPath) { - try { - await fs.access(`${repoPath}/config`) +const defaultRepo = (type) => { + path.join( + os.homedir(), + type === 'js' ? '.jsipfs' : '.ipfs' + ) +} - return true +const checkForRunningApi = (path) => { + let api + try { + api = fs.readFileSync(`${path}/api`) } catch (err) { - return false + log(`Unable to open api file: ${err}`) } + + return api ? api.toString() : null } module.exports = { - createTempRepoPath, removeRepo, - repoExists + repoExists, + defaultRepo, + checkForRunningApi } diff --git a/test/add-retrieve.spec.js b/test/add-retrieve.spec.js index f7d99a68..98d34207 100644 --- a/test/add-retrieve.spec.js +++ b/test/add-retrieve.spec.js @@ -14,30 +14,24 @@ const tests = [ { type: 'proc', exec: JSIPFS, bits: 512 } ] -describe('data can be put and fetched', () => { +describe('data can be put and fetched', function () { + this.timeout(10000) tests.forEach((dfOpts) => describe(`${dfOpts.type}`, () => { let ipfsd before(async function () { - this.timeout(30 * 1000) - const f = IPFSFactory.create(dfOpts) - ipfsd = await f.spawn({ initOptions: { bits: dfOpts.bits, profile: 'test' } }) + ipfsd = await f.spawn({ initOptions: { profile: 'test' } }) expect(ipfsd).to.exist() expect(ipfsd.api).to.exist() expect(ipfsd.api).to.have.property('id') }) - after(async function () { - this.timeout(20 * 1000) - await ipfsd.stop() - }) + after(() => ipfsd.stop()) it('put and fetch a block', async function () { - this.timeout(20 * 1000) - const data = Buffer.from('blorb') const block = await ipfsd.api.block.put(data) const cidStr = block.cid.toBaseEncodedString() diff --git a/test/endpoint/client.js b/test/endpoint/client.js index fee063df..314fbc48 100644 --- a/test/endpoint/client.js +++ b/test/endpoint/client.js @@ -32,14 +32,13 @@ describe('client', () => { it('should handle valid request', async () => { mock.post('http://localhost:9999/spawn', (req) => { - expect(req.body.options.opt1).to.equal('hello!') + expect(req.body.opt1).to.equal('hello!') return { body: { - id: hat(), - api: { - apiAddr: '/ip4/127.0.0.1/tcp/5001', - gatewayAddr: '/ip4/127.0.0.1/tcp/8080' - } + _id: hat(), + apiAddr: '/ip4/127.0.0.1/tcp/5001', + gatewayAddr: '/ip4/127.0.0.1/tcp/8080', + started: true } } }) @@ -57,7 +56,7 @@ describe('client', () => { mock.clearRoutes() }) - it('should handle valid request', async () => { + it('should handle invalid request', async () => { const badReq = boom.badRequest() mock.post('http://localhost:9999/spawn', () => { @@ -108,6 +107,8 @@ describe('client', () => { it('should handle valid request', async () => { const badReq = boom.badRequest() + // reset node + node.initialized = false mock.post('http://localhost:9999/init', () => { return { @@ -151,6 +152,9 @@ describe('client', () => { it('should handle invalid request', async () => { const badReq = boom.badRequest() + // reset node + node.clean = false + mock.post('http://localhost:9999/cleanup', () => { return { status: badReq.output.statusCode, @@ -204,6 +208,8 @@ describe('client', () => { it('should handle invalid request', async () => { const badReq = boom.badRequest() + // reset node + node.started = false mock.post('http://localhost:9999/start', () => { return { @@ -246,7 +252,8 @@ describe('client', () => { it('should handle invalid request', async () => { const badReq = boom.badRequest() - + // reset node + node.started = true mock.post('http://localhost:9999/stop', () => { return { status: badReq.output.statusCode, @@ -288,7 +295,8 @@ describe('client', () => { it('should handle invalid request', async () => { const badReq = boom.badRequest() - + // reset node + node.started = true mock.post('http://localhost:9999/stop', () => { return { status: badReq.output.statusCode, diff --git a/test/endpoint/routes.js b/test/endpoint/routes.js index c920f67a..8612e46f 100644 --- a/test/endpoint/routes.js +++ b/test/endpoint/routes.js @@ -75,15 +75,14 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'POST', - url: '/spawn', - headers: { 'content-type': 'application/json' } + url: '/spawn' }) expect(res.statusCode).to.equal(200) - expect(res.result.id).to.exist() - expect(res.result.api.apiAddr).to.exist() - expect(res.result.api.gatewayAddr).to.exist() + expect(res.result._id).to.exist() + expect(res.result.apiAddr).to.exist() + expect(res.result.gatewayAddr).to.exist() - id = res.result.id + id = res.result._id }) }) @@ -91,9 +90,7 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'GET', - url: `/api-addr?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/api-addr?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -103,8 +100,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'GET', - url: '/api-addr', - headers: { 'content-type': 'application/json' } + url: '/api-addr' }) expect(res.statusCode).to.equal(400) @@ -115,9 +111,7 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'GET', - url: `/getaway-addr?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/getaway-addr?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -127,8 +121,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'GET', - url: '/getaway-addr', - headers: { 'content-type': 'application/json' } + url: '/getaway-addr' }) expect(res.statusCode).to.equal(400) @@ -139,9 +132,7 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'POST', - url: `/init?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/init?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -150,8 +141,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'POST', - url: '/init', - headers: { 'content-type': 'application/json' } + url: '/init' }) expect(res.statusCode).to.equal(400) @@ -162,9 +152,7 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'POST', - url: `/cleanup?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/cleanup?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -173,8 +161,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'POST', - url: '/cleanup', - headers: { 'content-type': 'application/json' } + url: '/cleanup' }) expect(res.statusCode).to.equal(400) @@ -185,9 +172,7 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'POST', - url: `/start?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/start?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -196,8 +181,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'POST', - url: '/start', - headers: { 'content-type': 'application/json' } + url: '/start' }) expect(res.statusCode).to.equal(400) @@ -208,9 +192,7 @@ describe('routes', () => { it('should return 200 without timeout', async () => { const res = await server.inject({ method: 'POST', - url: `/stop?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/stop?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -220,8 +202,7 @@ describe('routes', () => { const res = await server.inject({ method: 'POST', url: `/stop?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id, timeout: 1000 } + payload: { timeout: 1000 } }) expect(res.statusCode).to.equal(200) @@ -230,8 +211,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'POST', - url: '/stop', - headers: { 'content-type': 'application/json' } + url: '/stop' }) expect(res.statusCode).to.equal(400) @@ -242,9 +222,7 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'POST', - url: `/kill?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/kill?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -254,8 +232,7 @@ describe('routes', () => { const res = await server.inject({ method: 'POST', url: `/kill?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id, timeout: 1000 } + payload: { timeout: 1000 } }) expect(res.statusCode).to.equal(200) @@ -264,8 +241,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'POST', - url: '/kill', - headers: { 'content-type': 'application/json' } + url: '/kill' }) expect(res.statusCode).to.equal(400) @@ -276,9 +252,7 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'GET', - url: `/pid?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/pid?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -287,8 +261,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'GET', - url: '/pid', - headers: { 'content-type': 'application/json' } + url: '/pid' }) expect(res.statusCode).to.equal(400) @@ -299,9 +272,7 @@ describe('routes', () => { it('should return 200', async () => { const res = await server.inject({ method: 'GET', - url: `/config?id=${id}`, - headers: { 'content-type': 'application/json' }, - payload: { id } + url: `/config?id=${id}` }) expect(res.statusCode).to.equal(200) @@ -310,8 +281,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'GET', - url: '/config', - headers: { 'content-type': 'application/json' } + url: '/config' }) expect(res.statusCode).to.equal(400) @@ -333,8 +303,7 @@ describe('routes', () => { it('should return 400', async () => { const res = await server.inject({ method: 'PUT', - url: '/config', - headers: { 'content-type': 'application/json' } + url: '/config' }) expect(res.statusCode).to.equal(400) diff --git a/test/non-disposable.spec.js b/test/non-disposable.spec.js new file mode 100644 index 00000000..03a19b7f --- /dev/null +++ b/test/non-disposable.spec.js @@ -0,0 +1,147 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const isNode = require('detect-node') +const IPFSFactory = require('../src') + +const expect = chai.expect +chai.use(dirtyChai) + +const tests = [ + { type: 'go', bits: 1024 }, + { type: 'js', bits: 512 }, + { type: 'proc', exec: require('ipfs'), bits: 512 } +] + +tests.forEach((fOpts) => { + describe(`non-disposable ${fOpts.type} daemon`, function () { + this.timeout(40000) + let daemon = null + let id = null + + before(async () => { + // Start a go daemon for attach tests + const f = IPFSFactory.create({ type: 'go' }) + daemon = await f.spawn() + id = await daemon.api.id() + }) + + after(() => daemon.stop()) + + it('should fail when passing initOptions to a initialized repo', async function () { + if (fOpts.type === 'proc' && !isNode) { + return this.skip() + } + const df = IPFSFactory.create(fOpts) + try { + await df.spawn({ + repoPath: daemon.path, + disposable: false, + init: true + }) + throw new Error('Should throw') + } catch (err) { + expect(err, err.message).to.exist() + } + }) + + it('should attach to initialized and running node', async function () { + if (fOpts.type === 'proc' && !isNode) { + return this.skip() + } + + const df = IPFSFactory.create(fOpts) + const ipfsd = await df.spawn({ + repoPath: daemon.path, + disposable: false, + init: true, + start: true + }) + + const data = await ipfsd.api.id() + expect(data.id).to.be.eq(id.id) + }) + + it('should attach to running node with manual start', async function () { + if (fOpts.type === 'proc' && !isNode) { + return this.skip() + } + const df = IPFSFactory.create(fOpts) + const ipfsd = await df.spawn({ + repoPath: daemon.path, + disposable: false, + init: true + }) + + const api = await ipfsd.start() + expect(daemon.apiAddr).to.be.eql(ipfsd.apiAddr) + expect(api).to.exist() + }) + + it('should not init and start', async () => { + const df = IPFSFactory.create(fOpts) + const path = await df.tmpDir(fOpts.type === 'js') + const ipfsd = await df.spawn({ + initOptions: { bits: fOpts.bits }, + repoPath: path, + disposable: false + }) + expect(ipfsd.api).to.not.exist() + expect(ipfsd.initialized).to.be.false() + expect(ipfsd.started).to.be.false() + await ipfsd.stop() + await ipfsd.cleanup() + }) + + it('should init and start', async () => { + const df = IPFSFactory.create(fOpts) + const path = await df.tmpDir(fOpts.type === 'js') + const ipfsd = await df.spawn({ + initOptions: { bits: fOpts.bits }, + repoPath: path, + disposable: false, + start: true, + init: true + }) + expect(ipfsd.api).to.exist() + expect(ipfsd.initialized).to.be.true() + expect(ipfsd.started).to.be.true() + await ipfsd.stop() + await ipfsd.cleanup() + }) + + it('should only init', async () => { + const df = IPFSFactory.create(fOpts) + const path = await df.tmpDir(fOpts.type === 'js') + const ipfsd = await df.spawn({ + initOptions: { bits: fOpts.bits }, + repoPath: path, + disposable: false, + init: true + }) + expect(ipfsd.initialized).to.be.true() + expect(ipfsd.started).to.be.false() + // await ipfsd.stop() + // await ipfsd.cleanup() + }) + + it('should only init manualy', async () => { + const df = IPFSFactory.create(fOpts) + const path = await df.tmpDir(fOpts.type === 'js') + const ipfsd = await df.spawn({ + initOptions: { bits: fOpts.bits }, + repoPath: path, + disposable: false + }) + expect(ipfsd.initialized).to.be.false() + await ipfsd.init() + expect(ipfsd.initialized).to.be.true() + expect(ipfsd.started).to.be.false() + // await ipfsd.stop() + // await ipfsd.cleanup() + }) + }) +}) diff --git a/test/spawn-options.spec.js b/test/spawn-options.spec.js index a469e77c..5c68d185 100644 --- a/test/spawn-options.spec.js +++ b/test/spawn-options.spec.js @@ -7,11 +7,11 @@ const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) -const fs = require('fs-extra') const isNode = require('detect-node') const hat = require('hat') const IPFSFactory = require('../src') const JSIPFS = require('ipfs') +const { repoExists } = require('./../src/utils/repo/nodejs') const tests = [ { type: 'go', bits: 1024 }, { type: 'js', bits: 512 }, @@ -26,7 +26,7 @@ const versions = { } describe('Spawn options', function () { - this.timeout(20 * 1000) + this.timeout(60 * 1000) tests.forEach((fOpts) => describe(`${fOpts.type}`, () => { const VERSION_STRING = versions[fOpts.type] @@ -37,8 +37,6 @@ describe('Spawn options', function () { }) it('f.version', async function () { - this.timeout(20 * 1000) - let version = await f.version({ type: fOpts.type, exec: fOpts.exec @@ -51,107 +49,12 @@ describe('Spawn options', function () { expect(version).to.be.eql(VERSION_STRING) }) - describe('init and start', () => { - let prevRepoPath - - describe('init and start manually', () => { - let ipfsd - let repoPath - - before(async () => { - const tmpDir = await f.tmpDir(fOpts.type) - - repoPath = tmpDir - prevRepoPath = repoPath - }) - - it('f.spawn', async () => { - const options = { - repoPath: repoPath, - init: false, - start: false, - disposable: false, - initOptions: { bits: fOpts.bits, profile: 'test' } - } - - ipfsd = await f.spawn(options) - expect(ipfsd).to.exist() - expect(ipfsd.api).to.not.exist() - expect(ipfsd.initialized).to.eql(false) - - repoPath = ipfsd.repoPath - }) - - it('ipfsd.init', async function () { - this.timeout(20 * 1000) - - await ipfsd.init() - expect(ipfsd.initialized).to.be.ok() - }) - - it('ipfsd.start', async function () { - this.timeout(20 * 1000) - - const api = await ipfsd.start() - expect(api).to.exist() - expect(api.id).to.exist() - }) - - it('ipfsd.stop', async function () { - this.timeout(20 * 1000) - - await ipfsd.stop() - }) - }) - - describe('spawn from an initialized repo', () => { - let ipfsd - - it('f.spawn', async function () { - this.timeout(20 * 1000) - - ipfsd = await f.spawn({ - repoPath: prevRepoPath, - init: false, - start: false, - disposable: false - }) - expect(ipfsd).to.exist() - expect(ipfsd.api).to.not.exist() - expect(ipfsd.initialized).to.eql(true) - }) - - it('ipfsd.start', async function () { - this.timeout(20 * 1000) - - const api = await ipfsd.start() - expect(api).to.exist() - expect(api.id).to.exist() - }) - - it('ipfsd.stop', async function () { - this.timeout(20 * 1000) - - await ipfsd.stop() - }) - }) - }) - describe('spawn a node and attach api', () => { - let ipfsd - it('create init and start node', async function () { - this.timeout(20 * 1000) - - ipfsd = await f.spawn({ initOptions: { bits: fOpts.bits, profile: 'test' } }) + const ipfsd = await f.spawn({ initOptions: { bits: fOpts.bits, profile: 'test' } }) expect(ipfsd).to.exist() expect(ipfsd.api).to.exist() expect(ipfsd.api.id).to.exist() - }) - - it('ipfsd.stop', async function () { - this.timeout(20 * 1000) - await ipfsd.stop() }) }) @@ -201,17 +104,7 @@ describe('Spawn options', function () { }) describe('custom config options', () => { - let ipfsd - - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) - it('custom config', async function () { - this.timeout(50 * 1000) - const addr = '/ip4/127.0.0.1/tcp/5678' const swarmAddr1 = '/ip4/127.0.0.1/tcp/35666' const config = { @@ -224,14 +117,13 @@ describe('Spawn options', function () { Bootstrap: ['/dns4/wss0.bootstrap.libp2p.io/tcp/443/wss/ipfs/QmZMxNdpMkewiVZLMRxaNxUeZpDUb34pWjZ1kZvsd16Zic'] } - ipfsd = await f.spawn({ + const ipfsd = await f.spawn({ config: config, initOptions: { bits: fOpts.bits, profile: 'test' } }) - const apiConfig = await ipfsd.getConfig('Addresses.API') expect(apiConfig).to.eql(addr) @@ -251,55 +143,27 @@ describe('Spawn options', function () { } expect(bootstrapConfig).to.deep.equal(config.Bootstrap) + await ipfsd.stop() }) }) describe('custom repo path', () => { - // TODO why wouldn't this work from the browser if we use the - // remote endpoint? + // We can only check if it really got created when run in Node.js if (!isNode) { return } - let ipfsd - let repoPath - - before(async () => { - repoPath = await f.tmpDir(fOpts.type) - }) - it('allows passing custom repo path to spawn', async function () { - this.timeout(20 * 1000) - - const config = { - Addresses: { - Swarm: [ - '/ip4/127.0.0.1/tcp/0/ws', - '/ip4/127.0.0.1/tcp/0' - ], - API: '/ip4/127.0.0.1/tcp/0', - Gateway: '/ip4/127.0.0.1/tcp/0' - } - } - + const repoPath = await f.tmpDir(fOpts.type) const options = { disposable: false, - init: false, - start: false, + init: true, + start: true, repoPath: repoPath, - config: config, initOptions: { bits: fOpts.bits, profile: 'test' } } - ipfsd = await f.spawn(options) - await ipfsd.init() - await ipfsd.start() - - if (isNode) { - // We can only check if it really got created when run in Node.js - expect(fs.existsSync(repoPath)).to.be.ok() - } - }) - - after(async () => { + const ipfsd = await f.spawn(options) + const exists = await repoExists(repoPath) + expect(exists).to.be.true() await ipfsd.stop() await ipfsd.cleanup() }) @@ -308,25 +172,18 @@ describe('Spawn options', function () { describe('f.spawn with args', () => { if (!isNode && fOpts.type !== 'proc') { return } - let ipfsd - - it('spawn with pubsub', async function () { - this.timeout(20 * 1000) - + it('check that pubsub was enabled', async () => { + const topic = `test-topic-${hat()}` + const data = Buffer.from('hey there') const options = { args: ['--enable-namesys-pubsub'], initOptions: { bits: fOpts.bits, profile: 'test' } } - ipfsd = await f.spawn(options) - }) - - it('check that pubsub was enabled', () => { - const topic = `test-topic-${hat()}` - const data = Buffer.from('hey there') + const ipfsd = await f.spawn(options) return new Promise(async (resolve, reject) => { // eslint-disable-line no-async-promise-executor - const handler = (msg) => { + const handler = async (msg) => { try { expect(msg.data).to.eql(data) expect(msg).to.have.property('seqno') @@ -335,25 +192,21 @@ describe('Spawn options', function () { resolve() } catch (err) { reject(err) + } finally { + await ipfsd.stop() } } await ipfsd.api.pubsub.subscribe(topic, handler) - ipfsd.api.pubsub.publish(topic, data) + await ipfsd.api.pubsub.publish(topic, data) }) }) - - it('ipfsd.stop', async function () { - this.timeout(20 * 1000) - await ipfsd.stop() - }) }) describe('change config while running', () => { let ipfsd before(async function () { - this.timeout(20 * 1000) ipfsd = await f.spawn({ initOptions: { bits: fOpts.bits, @@ -363,7 +216,6 @@ describe('Spawn options', function () { }) after(async function () { - this.timeout(20 * 1000) await ipfsd.stop() }) @@ -378,8 +230,6 @@ describe('Spawn options', function () { }) it('Should set a config value', async function () { - this.timeout(20 * 1000) - await ipfsd.setConfig('Bootstrap', 'null') const res = await ipfsd.getConfig('Bootstrap') @@ -396,7 +246,7 @@ describe('Spawn options', function () { await ipfsd.setConfig('Bootstrap', 'true') expect.fail('Should have errored') } catch (err) { - expect(err.message).to.contain('failed to set config value') + expect(err).to.exist() } }) }) diff --git a/test/start-stop.node.js b/test/start-stop.node.js index 3713dcaf..4c96f83a 100644 --- a/test/start-stop.node.js +++ b/test/start-stop.node.js @@ -289,51 +289,6 @@ tests.forEach((fOpts) => { }) }) - describe('should detect and attach to running node', () => { - let ipfsd - let exec - - before(async function () { - this.timeout(50 * 1000) - - const df = IPFSFactory.create(dfConfig) - exec = findIpfsExecutable(fOpts.type) - - ipfsd = await df.spawn({ - exec, - initOptions: { bits: fOpts.bits, profile: 'test' } - }) - - expect(ipfsd).to.exist() - }) - - after(async () => { - await ipfsd.stop() - }) - - it('should return a node', () => { - expect(ipfsd).to.exist() - }) - - it('should attach to running node', async function () { - this.timeout(50 * 1000) - - const df = IPFSFactory.create(dfConfig) - const daemon = await df.spawn({ - initOptions: { bits: fOpts.bits, profile: 'test' }, - repoPath: ipfsd.repoPath, - disposable: false - }) - - const api = await daemon.start() - - expect(api).to.exist() - expect(ipfsd.apiAddr).to.be.eql(daemon.apiAddr) - - await daemon.stop() - }) - }) - describe('should fail on invalid exec path', function () { this.timeout(20 * 1000)