diff --git a/.gitignore b/.gitignore index a68f6491c0..06eea61ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ test/repo-tests* **/bundle.js docs +.vscode +.eslintrc # Logs logs *.log diff --git a/package.json b/package.json index 707ca981ea..4de4f0a7f7 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "ipfs-block": "~0.7.1", "ipfs-block-service": "~0.14.0", "ipfs-multipart": "~0.1.0", - "ipfs-repo": "0.20.0", + "ipfs-repo": "~0.21.0", "ipfs-unixfs": "~0.1.14", "ipfs-unixfs-engine": "~0.29.0", "ipld": "~0.17.0", diff --git a/src/cli/bin.js b/src/cli/bin.js index 2ccf2f66a0..c07ac76aa6 100755 --- a/src/cli/bin.js +++ b/src/cli/bin.js @@ -2,104 +2,159 @@ 'use strict' -const yargs = require('yargs') -const updateNotifier = require('update-notifier') -const readPkgUp = require('read-pkg-up') const fs = require('fs') const path = require('path') -const utils = require('./utils') -const print = utils.print +const yargs = require('yargs/yargs') +const updateNotifier = require('update-notifier') +const readPkgUp = require('read-pkg-up') +const { disablePrinting, print, getNodeOrAPI } = require('./utils') const pkg = readPkgUp.sync({cwd: __dirname}).pkg + updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 * 7 // 1 week }).notify() -const args = process.argv.slice(2) +const MSG_USAGE = `Usage: +ipfs - Global p2p merkle-dag filesystem. + + ipfs [options] ...` + +const MSG_EPILOGUE = `Use 'ipfs --help' to learn more about each command. + +ipfs uses a repository in the local file system. By default, the repo is +located at ~/.ipfs. To change the repo location, set the $IPFS_PATH +environment variable: -// Determine if the first argument is a sub-system command +export IPFS_PATH=/path/to/ipfsrepo + +EXIT STATUS + +The CLI will exit with one of the following values: + +0 Successful execution. +1 Failed executions. +` +const MSG_NO_CMD = 'You need at least one command before moving on' + +const argv = process.argv.slice(2) const commandNames = fs.readdirSync(path.join(__dirname, 'commands')) -const isCommand = commandNames.includes(`${args[0]}.js`) +const isCommand = commandNames.includes(`${argv[0]}.js`) -const cli = yargs +let args = {} +let cli = yargs(argv) + .usage(MSG_USAGE) .option('silent', { desc: 'Write no output', type: 'boolean', default: false, - coerce: ('silent', silent => silent ? utils.disablePrinting() : silent) + coerce: disablePrinting + }) + .option('debug', { + desc: 'Show debug output', + type: 'boolean', + default: false, + alias: 'D' }) .option('pass', { desc: 'Pass phrase for the keys', type: 'string', default: '' }) + .option('api', { + desc: 'Use a specific API instance.', + type: 'string' + }) .commandDir('commands', { // Only include the commands for the sub-system we're using, or include all // if no sub-system command has been passed. include (path, filename) { if (!isCommand) return true - return `${args[0]}.js` === filename + return `${argv[0]}.js` === filename } }) - .epilog(utils.ipfsPathHelp) - .demandCommand(1) - .fail((msg, err, yargs) => { - if (err) { - throw err // preserve stack + + if(!isCommand){ + cli + // NOTE: This creates an alias of + // `jsipfs files {add, get, cat}` to `jsipfs {add, get, cat}`. + // This will stay until https://github.com/ipfs/specs/issues/98 is resolved. + .command(require('./commands/files/add')) + .command(require('./commands/files/cat')) + .command(require('./commands/files/get')) + } + cli + .demandCommand(1, MSG_NO_CMD) + .alias('help', 'h') + .epilogue(MSG_EPILOGUE) + .strict() + // .recommendCommands() + .completion() + +if (['daemon', 'init', 'id', 'version'].includes(argv[0])) { + args = cli.fail((msg, err, yargs) => { + if (err instanceof Error && err.message && !msg) { + msg = err.message } - if (args.length > 0) { - print(msg) + // Cli specific error messages + if (err && err.code === 'ERR_REPO_NOT_INITIALIZED') { + msg = `No IPFS repo found in ${err.path}. +please run: 'ipfs init'` } - yargs.showHelp() - }) + // Show help and error message + if (!args.silent) { + yargs.showHelp() + console.error('Error: ' + msg) + } -// If not a sub-system command then load the top level aliases -if (!isCommand) { - // NOTE: This creates an alias of - // `jsipfs files {add, get, cat}` to `jsipfs {add, get, cat}`. - // This will stay until https://github.com/ipfs/specs/issues/98 is resolved. - const addCmd = require('./commands/files/add') - const catCmd = require('./commands/files/cat') - const getCmd = require('./commands/files/get') - const aliases = [addCmd, catCmd, getCmd] - aliases.forEach((alias) => { - cli.command(alias.command, alias.describe, alias.builder, alias.handler) - }) -} + // Write to stderr when debug is on + if (err && args.debug) { + console.error(err) + } -// Need to skip to avoid locking as these commands -// don't require a daemon -if (args[0] === 'daemon' || args[0] === 'init') { - cli - .help() - .strict() - .completion() - .parse(args) + process.exit(1) + }).argv } else { - // here we have to make a separate yargs instance with - // only the `api` option because we need this before doing - // the final yargs parse where the command handler is invoked.. - yargs().option('api').parse(process.argv, (err, argv, output) => { - if (err) { - throw err - } - utils.getIPFS(argv, (err, ipfs, cleanup) => { - if (err) { throw err } - - cli - .help() - .strict() - .completion() - .parse(args, { ipfs: ipfs }, (err, argv, output) => { - if (output) { print(output) } - - cleanup(() => { - if (err) { throw err } + yargs() + .option('pass', { + desc: 'Pass phrase for the keys', + type: 'string', + default: '' + }) + .option('api', { + desc: 'Use a specific API instance.', + type: 'string' + }) + .parse(argv, (err, parsedArgv, output) => { + if (err) { + console.error(err) + } else { + getNodeOrAPI(parsedArgv) + .then(node => { + args = cli + .parse(argv, { ipfs: node }, (err, parsedArgv, output) => { + if (output) { + print(output) + } + if (node && node._repo && !node._repo.closed) { + node._repo.close(err => { + if (err) { + console.error(err) + } + }) + } + if (err && parsedArgv.debug) { + console.error(err) + } + }) + }) + .catch(err => { + console.error(err) + process.exit(1) }) - }) + } }) - }) } diff --git a/src/cli/commands/daemon.js b/src/cli/commands/daemon.js index 6c6e4d9d30..aa0d16cd67 100644 --- a/src/cli/commands/daemon.js +++ b/src/cli/commands/daemon.js @@ -2,10 +2,9 @@ const HttpAPI = require('../../http') const utils = require('../utils') +const promisify = require('promisify-es6') const print = utils.print -let httpAPI - module.exports = { command: 'daemon', @@ -31,21 +30,8 @@ module.exports = { handler (argv) { print('Initializing daemon...') - const repoPath = utils.getRepoPath() - httpAPI = new HttpAPI(process.env.IPFS_PATH, null, argv) - - httpAPI.start((err) => { - if (err && err.code === 'ENOENT' && err.message.match(/Uninitalized repo/i)) { - print('Error: no initialized ipfs repo found in ' + repoPath) - print('please run: jsipfs init') - process.exit(1) - } - if (err) { - throw err - } - print('Daemon is ready') - }) - + const httpAPI = new HttpAPI(process.env.IPFS_PATH, null, argv) + const start = promisify(httpAPI.start) const cleanup = () => { print(`Received interrupt signal, shutting down..`) httpAPI.stop((err) => { @@ -60,5 +46,7 @@ module.exports = { process.on('SIGTERM', cleanup) process.on('SIGINT', cleanup) process.on('SIGHUP', cleanup) + + return start().then(() => print('Daemon is ready')) } } diff --git a/src/cli/commands/id.js b/src/cli/commands/id.js index 81ed38eef4..dee40ea2f6 100644 --- a/src/cli/commands/id.js +++ b/src/cli/commands/id.js @@ -1,5 +1,6 @@ 'use strict' -const print = require('../utils').print + +const {print, getNodeOrAPI} = require('../utils') module.exports = { command: 'id', @@ -15,12 +16,11 @@ module.exports = { handler (argv) { // TODO: handle argv.format - argv.ipfs.id((err, id) => { - if (err) { - throw err - } - - print(JSON.stringify(id, '', 2)) - }) + return getNodeOrAPI(argv) + .then(node => Promise.all([node, node.id()])) + .then(([node, id]) => { + print(JSON.stringify(id, '', 2)) + node.clean() + }) } } diff --git a/src/cli/commands/init.js b/src/cli/commands/init.js index 36525419b4..ed0c5faa4e 100644 --- a/src/cli/commands/init.js +++ b/src/cli/commands/init.js @@ -2,8 +2,7 @@ const Repo = require('ipfs-repo') const IPFS = require('../../core') -const utils = require('../utils') -const print = utils.print +const { ipfsPathHelp, getRepoPath, print } = require('../utils') module.exports = { command: 'init', @@ -12,7 +11,7 @@ module.exports = { builder (yargs) { return yargs - .epilog(utils.ipfsPathHelp) + .epilog(ipfsPathHelp) .option('bits', { type: 'number', alias: 'b', @@ -22,33 +21,26 @@ module.exports = { .option('emptyRepo', { alias: 'e', type: 'boolean', - describe: "Don't add and pin help files to the local storage" + describe: 'Don\'t add and pin help files to the local storage' }) }, handler (argv) { - const path = utils.getRepoPath() + const path = getRepoPath() print(`initializing ipfs node at ${path}`) - const node = new IPFS({ + return IPFS.createNodePromise({ repo: new Repo(path), init: false, start: false - }) - - node.init({ - bits: argv.bits, - emptyRepo: argv.emptyRepo, - pass: argv.pass, - log: print - }, (err) => { - if (err) { - if (err.code === 'EACCES') { - err.message = `EACCES: permission denied, stat $IPFS_PATH/version` - } - throw err - } + }).then(node => { + return node.init({ + bits: argv.bits, + emptyRepo: argv.emptyRepo, + pass: argv.pass, + log: print + }) }) } } diff --git a/src/cli/commands/version.js b/src/cli/commands/version.js index 6bb8447a2f..775f07d407 100644 --- a/src/cli/commands/version.js +++ b/src/cli/commands/version.js @@ -1,7 +1,7 @@ 'use strict' const os = require('os') -const print = require('../utils').print +const {print, getNodeOrAPI} = require('../utils') module.exports = { command: 'version', @@ -33,27 +33,27 @@ module.exports = { }, handler (argv) { - argv.ipfs.version((err, data) => { - if (err) { - throw err - } + return getNodeOrAPI(argv, {forceRepoInitialized: false}) + .then(node => Promise.all([node, node.version()])) + .then(([node, data])=> { + const withCommit = argv.all || argv.commit + const parsedVersion = `${data.version}${withCommit ? `-${data.commit}` : ''}` - const withCommit = argv.all || argv.commit - const parsedVersion = `${data.version}${withCommit ? `-${data.commit}` : ''}` - - if (argv.repo) { + if (argv.repo) { // go-ipfs prints only the number, even without the --number flag. - print(data.repo) - } else if (argv.number) { - print(parsedVersion) - } else if (argv.all) { - print(`js-ipfs version: ${parsedVersion}`) - print(`Repo version: ${data.repo}`) - print(`System version: ${os.arch()}/${os.platform()}`) - print(`Node.js version: ${process.version}`) - } else { - print(`js-ipfs version: ${parsedVersion}`) - } - }) + print(data.repo) + } else if (argv.number) { + print(parsedVersion) + } else if (argv.all) { + print(`js-ipfs version: ${parsedVersion}`) + print(`Repo version: ${data.repo}`) + print(`System version: ${os.arch()}/${os.platform()}`) + print(`Node.js version: ${process.version}`) + } else { + print(`js-ipfs version: ${parsedVersion}`) + } + + node.clean(); + }) } } diff --git a/src/cli/utils.js b/src/cli/utils.js index f993e0caef..e10a986bc1 100644 --- a/src/cli/utils.js +++ b/src/cli/utils.js @@ -38,14 +38,15 @@ function getAPICtl (apiAddr) { return APIctl(apiAddr) } -exports.getIPFS = (argv, callback) => { +exports.getNodeOrAPI = (argv, stateOptions = {forceRepoInitialized: true}) => { + log('get node or api async') if (argv.api || isDaemonOn()) { - return callback(null, getAPICtl(argv.api), (cb) => cb()) + return Promise.resolve(getAPICtl(argv.api)) } // Required inline to reduce startup time const IPFS = require('../core') - const node = new IPFS({ + return IPFS.createNodePromise({ repo: exports.getRepoPath(), init: false, start: false, @@ -53,23 +54,7 @@ exports.getIPFS = (argv, callback) => { EXPERIMENTAL: { pubsub: true } - }) - - const cleanup = (cb) => { - if (node && node._repo && !node._repo.closed) { - node._repo.close(() => cb()) - } else { - cb() - } - } - - node.on('error', (err) => { - throw err - }) - - node.once('ready', () => { - callback(null, node, cleanup) - }) + }, stateOptions) } exports.getRepoPath = () => { @@ -77,7 +62,10 @@ exports.getRepoPath = () => { } let visible = true -exports.disablePrinting = () => { visible = false } +exports.disablePrinting = (silent) => { + visible = !silent + return silent +} exports.print = (msg, newline) => { if (newline === undefined) { diff --git a/src/core/boot.js b/src/core/boot.js index cc7c14970e..1fd81e19ba 100644 --- a/src/core/boot.js +++ b/src/core/boot.js @@ -36,15 +36,7 @@ module.exports = (self) => { } ], (err, res) => { if (err) { - // If the error is that no repo exists, - // which happens when the version file is not found - // we just want to signal that no repo exist, not - // fail the whole process. - // TODO: improve datastore and ipfs-repo implemenations so this error is a bit more unified - if (err.message.match(/not found/) || // indexeddb - err.message.match(/ENOENT/) || // fs - err.message.match(/No value/) // memory - ) { + if (err.code === 'ERR_REPO_NOT_INITIALIZED') { return cb(null, false) } return cb(err) @@ -72,7 +64,7 @@ module.exports = (self) => { // No repo, but need should init one if (doInit && !hasRepo) { tasks.push((cb) => self.init(initOptions, cb)) - // we know we will have a repo for all follwing tasks + // we know we will have a repo for all following tasks // if the above succeeds hasRepo = true } @@ -98,8 +90,12 @@ module.exports = (self) => { // Need to start up the node if (doStart) { if (!hasRepo) { - console.log('WARNING, trying to start ipfs node on uninitialized repo, maybe forgot to set "init: true"') - return done(new Error('Uninitalized repo')) + return done( + Object.assign(new Error('repo is not initialized yet'), { + code: 'ERR_REPO_NOT_INITIALIZED', + path: self._repo.path + }) + ) } else { tasks.push((cb) => self.start(cb)) } diff --git a/src/core/components/init.js b/src/core/components/init.js index 59555f383a..d50af6cfe0 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -27,11 +27,6 @@ module.exports = function init (self) { callback(null, res) } - if (self.state.state() !== 'uninitalized') { - return done(new Error('Not able to init from state: ' + self.state.state())) - } - - self.state.init() self.log('init') opts.emptyRepo = opts.emptyRepo || false diff --git a/src/core/components/repo.js b/src/core/components/repo.js index cf7004559c..158e6ac549 100644 --- a/src/core/components/repo.js +++ b/src/core/components/repo.js @@ -19,15 +19,7 @@ module.exports = function repo (self) { version: promisify((callback) => { self._repo._isInitialized(err => { if (err) { - // TODO: (dryajov) This is really hacky, there must be a better way - const match = [ - /Key not found in database \[\/version\]/, - /ENOENT/, - /not yet initialized/ - ].some((m) => { - return m.test(err.message) - }) - if (match) { + if (err.code === 'ERR_REPO_NOT_INITIALIZED') { // this repo has not been initialized return callback(null, repoVersion) } diff --git a/src/core/index.js b/src/core/index.js index 46f8c9bb2c..30b7cd4156 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -136,10 +136,74 @@ class IPFS extends EventEmitter { boot(this) } + + + /** + * Clean a node after usage, checks the current state and calls the needed methods + * Until _repo.close return a promise we just throw the error to perserve the stack + * + * @memberof IPFS + */ + clean() { + this.log('cleaning: state ', this.state.state()) + switch (this.state.state()) { + case 'stopped': + this._repo.close(err => { + if(err) { + throw err + } + }); + break; + case 'running': + this.stop(err => { + if(err) { + throw err + } + }) + break; + default: + break; + } + } } -exports = module.exports = IPFS +module.exports = IPFS -exports.createNode = (options) => { +IPFS.createNode = (options) => { return new IPFS(options) } + +/** + * Static factory method to create a node wrapped with a Promise + * + * The Promise waits for the Ready Event to resolve with a IPFS instance + * and rejects with Error Events. Rejections can be customized the with + * second param. + * + * @param {object} options - IPFS node options + * @param {object} stateOptions - Node state options to reject with error + * @returns {IPFS} + */ +IPFS.createNodePromise = (options = {}, stateOptions = {}) => { + return new Promise((resolve, reject) => { + const node = new IPFS(options) + + node.on('error', err => { + node.clean() + reject(err) + }) + + node.once('ready', () => { + if (stateOptions.forceRepoInitialized) { + node._repo._isInitialized((err) => { + if (err) { + reject(err) + } + resolve(node) + }) + } else { + resolve(node) + } + }) + }) +} diff --git a/src/http/index.js b/src/http/index.js index 93a5ec172e..178e867f37 100644 --- a/src/http/index.js +++ b/src/http/index.js @@ -81,7 +81,6 @@ function HttpApi (repo, config, cliArgs) { this.node.once('error', (err) => { this.log('error starting core', err) - err.code = 'ENOENT' cb(err) }) this.node.once('start', cb) diff --git a/test/cli/config.js b/test/cli/config.js index 2150c39e2c..7b96344f8e 100644 --- a/test/cli/config.js +++ b/test/cli/config.js @@ -58,7 +58,7 @@ describe('config', () => runOnAndOff((thing) => { }) it('set a config key with invalid json', () => { - return ipfs.fail('config foo {"bar:0} --json') + return ipfs.fail('config foo {\\"bar:0} --json') }) it('get a config key value', () => { diff --git a/test/cli/daemon.js b/test/cli/daemon.js index d60d591279..91ebc11c1b 100644 --- a/test/cli/daemon.js +++ b/test/cli/daemon.js @@ -118,10 +118,10 @@ describe('daemon', () => { it('gives error if user hasn\'t run init before', function (done) { this.timeout(100 * 1000) - const expectedError = 'no initialized ipfs repo found in ' + repoPath + const expectedError = 'No IPFS repo found in ' + repoPath ipfs('daemon').catch((err) => { - expect(err.stdout).to.have.string(expectedError) + expect(err.stderr).to.have.string(expectedError) done() }) })